【Nginx】“Nginx”初识

Nginx是一款由俄罗斯程序员Igor Sysoev所开发的轻量级WEB服务器反向代理服务器以及电子邮件(IMAP/POP3)代理服务器。相较于Apache、lighttpd具有占用内存少、稳定性高等优势,依靠其强大的并发能力、丰富的模板库以及友好灵活的配置而闻名。

Nginx的Master-Worker模式

启动nginx服务后,在80端口启动socket服务进行监听,可以使用netstat来查看这个监听端口:
在这里插入图片描述
Nginx之所以被称为 高性能服务器,这与他的设计架构与工作原理是密不可分的。如下图所示,它采用master进程和worker协同工作:
在这里插入图片描述
Nginx在启动之后,会有一个master进程和多个worker进程,两者的作用如下:

master进程:读取并配置文件nginx.conf;管理worker进程:向多个worker进程发送signal,监控workder进程的运行状态,当worker进程异常退出时,会自动启动新的worker进程。

worker进程:为了避免线程切换,每一个worker进程都维护一个线程处理连接和请求。多个worker进程之间是对等的,他们同等竞争来自client的请求,并且一个请求只能在一个worker进程中处理(进程间的相互独立性)。worker进程的个数由conf文件决定,一般和CPU的核心数一致。

如果你对上述文字描述不解,那我们模拟一下master-worker的工作原理流程图:
在这里插入图片描述
下述代码是从nginx的源码提取出来的master工作部分,可以看出,master进程中的for(;;)死循环内有一个关键的sigsuspend()函数调用,该函数调用使得master进程的大部分时间都处于挂起等待状态,直到master进程接收的信号为止:

void ngx_master_process_cycle(ngx_cycle_t *cycle)  
{  
    char              *title;  
    u_char            *p;  
    size_t             size;  
    ngx_int_t          i;  
    ngx_uint_t         n, sigio;  
    sigset_t           set;  
    struct itimerval   itv;  
    ngx_uint_t         live;  
    ngx_msec_t         delay;  
    ngx_listening_t   *ls;  
    ngx_core_conf_t   *ccf;  
  
    //信号处理设置工作  
    sigemptyset(&set);  
    sigaddset(&set, SIGCHLD);  
    sigaddset(&set, SIGALRM);  
    sigaddset(&set, SIGIO);  
    sigaddset(&set, SIGINT);  
    sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL));  
    sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL));  
    sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL));  
    sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL));  
    sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));  
    sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL));  
  
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {  
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,  
                      "sigprocmask() failed");  
    }  
  
    sigemptyset(&set);  
  
  
    size = sizeof(master_process);  
  
    for (i = 0; i < ngx_argc; i++) {  
        size += ngx_strlen(ngx_argv[i]) + 1;  
    }  
  
    title = ngx_pnalloc(cycle->pool, size);  
  
    p = ngx_cpymem(title, master_process, sizeof(master_process) - 1);  
    for (i = 0; i < ngx_argc; i++) {  
        *p++ = ' ';  
        p = ngx_cpystrn(p, (u_char *) ngx_argv[i], size);  
    }  
  
    ngx_setproctitle(title);  
  
  
    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);  
  
    //其中包含了fork产生子进程的内容  
    ngx_start_worker_processes(cycle, ccf->worker_processes,  
                               NGX_PROCESS_RESPAWN);  
    //Cache管理进程与cache加载进程的主流程  
    ngx_start_cache_manager_processes(cycle, 0);  
  
    ngx_new_binary = 0;  
    delay = 0;  
    sigio = 0;  
    live = 1;  
  
    for ( ;; ) {//循环  
        if (delay) {  
            if (ngx_sigalrm) {  
                sigio = 0;  
                delay *= 2;  
                ngx_sigalrm = 0;  
            }  
  
            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,  
                           "termination cycle: %d", delay);  
  
            itv.it_interval.tv_sec = 0;  
            itv.it_interval.tv_usec = 0;  
            itv.it_value.tv_sec = delay / 1000;  
            itv.it_value.tv_usec = (delay % 1000 ) * 1000;  
  
            if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {  
                ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,  
                              "setitimer() failed");  
            }  
        }  
  
        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");  
  
        sigsuspend(&set);//master进程休眠,等待接受信号被激活  
  
        ngx_time_update();  
  
        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,  
                       "wake up, sigio %i", sigio);  
  
        //标志位为1表示需要监控所有子进程  
        if (ngx_reap) {  
            ngx_reap = 0;  
            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "reap children");  
  
            live = ngx_reap_children(cycle);//管理子进程  
        }  
  
        //当live标志位为0(表示所有子进程已经退出)、ngx_terminate标志位为1或者ngx_quit标志位为1表示要退出master进程  
        if (!live && (ngx_terminate || ngx_quit)) {  
            ngx_master_process_exit(cycle);//退出master进程  
        }  
  
        //ngx_terminate标志位为1,强制关闭服务,发送TERM信号到所有子进程  
        if (ngx_terminate) {  
            if (delay == 0) {  
                delay = 50;  
            }  
  
            if (sigio) {  
                sigio--;  
                continue;  
            }  
  
            sigio = ccf->worker_processes + 2 /* cache processes */;  
  
            if (delay > 1000) {  
                ngx_signal_worker_processes(cycle, SIGKILL);  
            } else {  
                ngx_signal_worker_processes(cycle,  
                                       ngx_signal_value(NGX_TERMINATE_SIGNAL));  
            }  
  
            continue;  
        }  
  
        //ngx_quit标志位为1,优雅的关闭服务  
        if (ngx_quit) {  
            ngx_signal_worker_processes(cycle,  
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));//向所有子进程发送quit信号  
  
            ls = cycle->listening.elts;  
            for (n = 0; n < cycle->listening.nelts; n++) {//关闭监听端口  
                if (ngx_close_socket(ls[n].fd) == -1) {  
                    ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno,  
                                  ngx_close_socket_n " %V failed",  
                                  &ls[n].addr_text);  
                }  
            }  
            cycle->listening.nelts = 0;  
  
            continue;  
        }  
  
        //ngx_reconfigure标志位为1,重新读取配置文件  
        //nginx不会让原来的worker子进程再重新读取配置文件,其策略是重新初始化ngx_cycle_t结构体,用它来读取新的额配置文件  
        //再创建新的额worker子进程,销毁旧的worker子进程  
        if (ngx_reconfigure) {  
            ngx_reconfigure = 0;  
  
            //ngx_new_binary标志位为1,平滑升级Nginx  
            if (ngx_new_binary) {  
                ngx_start_worker_processes(cycle, ccf->worker_processes,  
                                           NGX_PROCESS_RESPAWN);  
                ngx_start_cache_manager_processes(cycle, 0);  
                ngx_noaccepting = 0;  
  
                continue;  
            }  
  
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");  
  
            //初始化ngx_cycle_t结构体  
            cycle = ngx_init_cycle(cycle);  
            if (cycle == NULL) {  
                cycle = (ngx_cycle_t *) ngx_cycle;  
                continue;  
            }  
  
            ngx_cycle = cycle;  
            ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,  
                                                   ngx_core_module);  
            //创建新的worker子进程  
            ngx_start_worker_processes(cycle, ccf->worker_processes,  
                                       NGX_PROCESS_JUST_RESPAWN);  
            ngx_start_cache_manager_processes(cycle, 1);  
  
            /* allow new processes to start */  
            ngx_msleep(100);  
  
            live = 1;  
            //向所有子进程发送QUIT信号  
            ngx_signal_worker_processes(cycle,  
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));  
        }  
        //ngx_restart标志位在ngx_noaccepting(表示正在停止接受新的连接)为1的时候被设置为1.  
        //重启子进程  
        if (ngx_restart) {  
            ngx_restart = 0;  
            ngx_start_worker_processes(cycle, ccf->worker_processes,  
                                       NGX_PROCESS_RESPAWN);  
            ngx_start_cache_manager_processes(cycle, 0);  
            live = 1;  
        }  
  
        //ngx_reopen标志位为1,重新打开所有文件  
        if (ngx_reopen) {  
            ngx_reopen = 0;  
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");  
            ngx_reopen_files(cycle, ccf->user);  
            ngx_signal_worker_processes(cycle,  
                                        ngx_signal_value(NGX_REOPEN_SIGNAL));  
        }  
  
        //平滑升级Nginx  
        if (ngx_change_binary) {  
            ngx_change_binary = 0;  
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "changing binary");  
            ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);  
        }  
  
        //ngx_noaccept为1,表示所有子进程不再处理新的连接  
        if (ngx_noaccept) {  
            ngx_noaccept = 0;  
            ngx_noaccepting = 1;  
            ngx_signal_worker_processes(cycle,  
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));  
        }  
    }  
}

相较于master,worker进程就显得简单多了:它的主要任务是完成具体的任务逻辑,即client或server之间的数据读取、I/O交互事件,所以worker进程的阻塞点是在像select()、epoll_wait()等这样的I/O多路复用函数调用处,以等待发生数据可读/写事件,以及被可能收到的进程信号中断。

static void ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)  
{  
    ngx_int_t      i;  
    ngx_channel_t  ch;  
  
    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start worker processes");  
  
    ch.command = NGX_CMD_OPEN_CHANNEL;  
  
    //循环创建n个worker子进程  
    for (i = 0; i < n; i++) {  
        //完成fok新进程的具体工作  
        ngx_spawn_process(cycle, ngx_worker_process_cycle,  
                          (void *) (intptr_t) i, "worker process", type);  
  
        //全局数组ngx_processes就是用来存储每个子进程的相关信息,如:pid,channel,进程做具体事情的接口指针等等,这些信息就是用结构体ngx_process_t来描述的。  
        ch.pid = ngx_processes[ngx_process_slot].pid;  
        ch.slot = ngx_process_slot;  
        ch.fd = ngx_processes[ngx_process_slot].channel[0];  
  
        /*在ngx_spawn_process创建好一个worker进程返回后,master进程就将worker进程的pid、worker进程在ngx_processes数组中的位置及channel[0]传递给前面已经创建好的worker进程,然后继续循环开始创建下一个worker进程。刚提到一个channel[0],这里简单说明一下:channel就是一个能够存储2个整型元素的数组而已,这个channel数组就是用于socketpair函数创建一个进程间通道之用的。master和worker进程以及wor的一个通道进行通信,这个通道就是在ngx_spawn_process函数中fork之前调用socketpair创建的。*/  
        ngx_pass_open_channel(cycle, &ch);  
    }
Nginx如何做到热部署

鉴于master管理进程worker工作进程分离设计,使得nginx具备热部署的功能。所谓热部署,就是对nginx.conf进行修改后,不需要restart nginx,也不需要中断请求,就能让配置文件生效。Nginx对此的做法是:在修改配置文件nginx.conf后,重新生成新的worker进程,当然会以新的配置进行处理,至于旧的worker进程,等执行完以前的请求后,发送信号kill即可。

因此在7*24小时不间断服务的前提下,就可以对Nginx服务器升级修改配置文件更换日志文件等操作。

Nginx的反向代理服务

在介绍反向代理之前,我先给大家科普一下正向代理它更像是一个跳板,代理访问目标资源。比如你想在Youtube上看视频、上Google搜索信息,但是直接访问肯定是不行的,它们的服务器都设立在国外,这时你需要连接上可以访问国外网站的代理服务器,通过代理服务器获取到资源后,然后返回给你。

总结来说正向代理是一个位于client和目标服务器之间的代理服务器,client向代理发送一个请求并指定目标服务器的ip和port,然后目标服务器将请求获取的内容通过代理再返回给client(注意:client需要设置代理的ip和port)。

正向代理的主要作用如下:

(1)访问国外资源,如Youtube、google

(2)可以做缓存,加速访问资源

(3)对客户端访问授权,上网进行认证

(4)代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息

反向代理:以代理服务器来接收Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给Internet上请求连接的client,此时代理服务器对外就表现为一个服务器。反向代理对外是透明的,访问者并不知道自己访问的是一个代理,client也是无感知代理的存在的,因此客户端是不需要任何配置就可以直接访问的。

反向代理的主要作用如下:

(1)保证内网的安全。可以使用反向代理提供WAF功能,阻止web攻击(大型网站通常将反向代理作为公网访问地址,将Web服务器作为内网)。
(2)负载均衡。当单机无法支撑一个网站应用时,就要考虑使用多台机器横向扩展的方式来处理多个请求,把请求分发多台机器上的技术就是负载均衡。

Nginx使用upstream来定义一组参与负载均衡的服务器(可以在nginx.conf的http段中配置,默认路径:/usr/local/nginx/conf/),我介绍一个简单的配置:

upstream 1024do.com {
      server  192.168.1.20;
      server  192.168.1.21;
      server  192.168.1.22;
}
 
server{
    listen 80;
    server_name 1024do.com;
    location / {
        proxy_pass https://1024do.com;
    }
}

上述配置定义了三个服务器,然后在server配置段中使用proxy_pass来定义使用的服务器组,就非常容易的将1024do.com这个站点配置成了负载均衡的。上述的配置默认是按顺序轮询,因服务器的所处位置硬件性能`等可以配置的更灵活。

Nginx的负载均衡可以划分为两大类:内置策略和扩展策略内置策略包含加权轮询和IP hash,在默认情况下会编译进Nginx内核,只需在Nginx配置中指明参数。扩展策略有第三方模块策略:fair、url hash等。下面主要分析一下内置策略:

(1)加权轮询策略

upstream 1024do.com {
      server  192.168.1.20 weight=3;
      server  192.168.1.21 weight=1;
      server  192.168.1.22 weight=5;
}

在上述的服务器列表后面加上weight参数来设置权重数字越大权重越大,分配的请求就越多。加权轮询策略不依赖于客户端的任何信息,完全依靠后端服务器的情况来进行选择,但是同一个客户端的多次请求可能会被分配到不同的后端服务器进行处理,无法满足做会话保持的需求。

需要注意的是,Nginx每次选出的服务器并不一定是当前权重最大的,整体上是根据服务器的权重在各个服务器上按照比例分布的。

(2)IP Hash策略

这种轮询策略是将请求ip和服务器建立起稳固的关系,每个请求按访问ip进行hash分配,这样每个client会固定访问一个后端服务器。因此IP Hash可以轻松的解决负载均衡时单机session变化的问题。

upstream 1024do.com {
      ip_hash;
      server  192.168.1.20;
      server  192.168.1.21;
      server  192.168.1.22;
}

IP Hash虽然解决了会话保持的需求,但是如果hash后的结果拥挤在一台服务器上时,将导致某台服务器的压力非常大。如果仅仅为了会话保持,可以考虑将session迁移至数据库。

关于负载均衡服务器的主要配置参数如下:

1)自定义端口
server 192.168.1.24:8080; 

2)使用down参数指定服务器不参与分发请求
server 192.168.2.24 down; 

3)backup指定候补服务器

正常情况下不会使用候补服务器,只有后端服务器比较繁忙或压力大时才会使用。
server 192.168.3.21 backup; 
4)max_fairs审核服务器的健康状况

max_fairs可以设定一个请求失败的次数,超过限度则被认为服务器不可用,不再分发请求到此服务器上。
server 192.168.21.169 max_fairs=3; 
Nginx的epoll模型

谈及epoll就不得不提select、poll两种事件驱动。

Nginx的诞生主要是为了解决C10k问题,这与它设计之初的架构是分不开的。在Linux早期很长一段时间内都是使用select来监听事件的,直到Linux2.6内核才提出了epoll,它也是Nginx之所以高并发、高性能的核心。

epoll不是使用一个函数,而是使用C库封装的3个接口:

1.int epoll_create(int size);

功能:创建epoll模型,并返回新的epoll对象的文件描述符。这个文件描述符用于后续的epoll操作;
     如果不需要使用这个描述符,请使用close关闭;

参数:
     size:内核保证能够正确处理的最大句柄数,不起实际作用;

返回值:成功返回一个非负数(实际为文件描述符),失败返回-1并设置error;


2.int epoll_ctl(int epfd, int op, int fd, struct epoll_envent* event);

功能:维护epoll模型,新增、删除、修改特定的事件。例如,将刚建立的socket加入到epoll中让其监控,
     或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等(也就是将I/O流放到内核);

参数: 
     epfd:待操作的内核事件表的文件描述符;
     
     op:指定操作的类型,分别有三种:
                      EPOLL_CTL_ADD:注册;
                      EPOLL_CTL_MOD:修改; 
                      EPOLL_CTL_DEL:删除;

     fd:待操作的文件描述符;

     event:用来指定事件,它是epoll_event结构指针类型

            struct epoll_event {
                  _uint32_t events; //epoll事件
                  epoll_data_t data; //用户数据
            }

返回值:成功返回0,失败返回-1并设置error;
 

3.int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);

功能:等待事件发生;

参数:
     epfd: 待监测的内核事件表; 

     events:表示一个结构体数组,是一个输出型参数,用来获取从已经就绪的事件的相关信息。 
             events不可以是空指针,内核只负责把数据复制到这个events数组中,而不会去
             帮助我们在用户态中分配内存;

     maxevents:指明events的大小,每次能处理的事件数的大小,值不能大于epoll_create的size;

     timeout:设置超时时间;-1:永不超时,直到有事件产生才触发;0:立即返回

返回值:成功返回就绪文件描述符的个数,失败返回-1并设置error;

epoll如何巧妙利用上述三个接口在User Space和Kernel Space中提高并发与性能?我们来看一张图:
在这里插入图片描述
上图未对具体操作说明,下面我补充一下:

步骤一:首先执行eoll_create在内核维护一块epoll的高速cache区,并在该缓冲区建立红黑树和就绪链表,用户传入的文件句柄将被放到红黑树中(这也是第一次拷贝)。

步骤二:内核针对读缓冲区和写缓冲区来判断是否可读可写,这个动作与epoll无关。

步骤三:epoll_ctl执行EPOLL_CTL_ADD动作时除了将文件句柄挂在红黑树上之外,还向内核注册了该文件句柄的回调函数,内核在检测到某句柄可读可写时则调用该回调函数,回调函数将文件句柄挂到就绪链表上。

步骤四:epoll_wait负责监控就绪链表,如果就绪链表存在文件句柄,则表示该文件句柄可读可写,则返回到用户态(少量的二次拷贝)。

步骤五:由于内核不修改文件句柄的位,因此只需要在第一次传入时就可以重复监控,直到使用epoll_ctl删除,否则不需要重新传入,因此无需多次拷贝。

简单来说,epoll是继承了select、poll的I/O多路复用的思想,并在二者的基础上从监控I/O流,查找I/O事件等角度来提高效率,从内核句柄列表到红黑书,再到就绪链表来实现的。

总结一下epoll的优点如下:

1)监视的fd数量不受限制。它所支持的fd上限是最大能打开文件的数目,受限于内存大小。具体数目可以在/proc/sys/fs/file-max目录下查看。

(2)IO的效率不会随着监视fd的数量增多而下降。epoll不同于select和poll的轮询方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。

(3)mmap加速内核与用户空间的信息传递。epoll是通过内核与用户空间内存映射的同一块内存,避免了无谓的内存拷贝。

(4)获取就绪事件的时间复杂度为O(1)。调用epoll_wait时,无需遍历,只要list中有数据就返回,没有数据就sleep,等到timeout后即使list有数据也返回。

(5)支持ET模式。下列图示并说明一下epoll的两种模式:

在这里插入图片描述
LT(level triggered):作为epoll缺省的工作模式,同时支持阻塞和非阻塞方式。LT模式下,内核告诉你一个文件描述符是否就绪了,然后你可以处理这个就绪事件,如果你不做任何操作,内核会继续通知你,直到事件被处理,在一定程序上降低了出错率。

ET(edge triggered):Nginx的默认工作模式,仅支持非阻塞方式。与LT的区别在于,当一个新的事件到来时,ET模式下可以从epoll_wait中获取到这个事件,应用程序应该立即处理该事件,因为epoll_wait后续调用将不再通知此事件(因此ET模式下缓冲区数据要一次性读干净,防止其他事件得不到处理)。ET模式在很大程度上降低了同一个事件被多次触发的可能,因此更加高效。

Keepalived实现Nginx高可用

Nginx作为入口网关,如果出现单点问题,显然是不可接受的,因此Keepalived应运而生(当然还有heartbeat,corosync等)。Keepalived作为一个高可用的解决方案,主要是用来防止服务器单点发生故障,可以通过和Nginx配合来实现Web服务的高可用。

Keepalived是以VRRP(Virtual Router Redundancy Protocol)协议来实现的,即虚拟路由冗余协议。它是将多台提供相同功能的路由器构成一个路由器组,这个组里面存在一个master和多个backup,master上有一个对外提供服务的虚拟ip,它会发组播给backup,当backup收不到VRRP包时就认为master宕掉了,需要根据VRRP的优先级来选举一个backup充当master。

VRRP的工作逻辑如下图:
在这里插入图片描述
介绍完VRRP之后,我们回到Keepalived实现Nginx的高可用上,它的实现思路主要包含两步:

① 请求不会直接打到Nginx上,会先通过虚拟IP;

② Keepalived可以监控Nginx的生命状态,提供一个用户自定义的脚本,来定期检查Nginx进程的状态,进行权重变化,从而实现Nginx故障切换。
在这里插入图片描述
简单来说,外界过来一个request请求会先通过VRRP得到虚拟IP,虚拟IP通过脚本来检测Nginx进程的健康状况,当Nginx1发生故障时,会将资源切换至备用的Nginx2上,从而体现Nginx的可高用性。

总结:本篇主要从Nginx的稳定性、高并发、高性能以及高可用四个方面进行了剖析,理解透这些相信你对Nginx会有更为清晰的认识,试着将这些原理运用于实际之中吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农印象

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值