├── 2 ├── 2.1 启动流程.md ├── 2.10 Http请求处理.md ├── 2.2 cycle初始化.md ├── 2.3 模块初始化.md ├── 2.4 配置文件解析.md ├── 2.5 多进程启动.md ├── 2.6 事件驱动详解.md ├── 2.7 Event模块详解.md ├── 2.8 Epoll模块详解.md └── 2.9 Http模块详解.md ├── 3 ├── 3.1 ZAB协议详解.md ├── 3.2 Watcher机制.md ├── 3.3 Server启动流程.md ├── 3.4 Server详解.md ├── 3.5 Leader选举.md ├── 3.6 会话管理.md ├── 3.7 事务请求处理.md └── 3.8 数据与存储.md ├── 4 ├── 4.1 Kafka简介.md ├── 4.2 Broker详解.md ├── 4.3 Producer详解.md ├── 4.4 高级消费者-初始化.md ├── 4.5 高级消费者-拉取消息.md ├── 4.6 高级消费者-负载均衡.md ├── 4.7 高级消费者-offset管理.md └── 4.8 GroupCoordinator.md ├── 5 ├── 5.1 类型系统.md ├── 5.2 指针与可寻址性.md ├── 5.3 协程和通道.md └── 5.4 接口和反射.md ├── 6 └── 6.1 总览.md ├── 7 ├── 7.1 Mysql总览.md ├── 7.2 索引详解.md ├── 7.3 查询详解.md ├── 7.4 事务详解.md ├── 7.5 连接详解.md ├── 7.6 子查询.md ├── 7.7 分库分表.md └── 7.8 大表优化.md ├── .gitignore ├── .idea ├── backend.iml ├── codeStyles │ └── Project.xml ├── dbnavigator.xml ├── encodings.xml ├── markdown-navigator.xml ├── markdown-navigator │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── vcs.xml └── workspace.xml ├── LICENSE ├── SUMMARY.md └── img ├── 2-10-handle-http-request.png ├── 2-3-ngx-module-conf.png ├── 2-4-conf-scopes.png ├── 2-4-ngx-conf.png ├── 2-7-ngx-event-conf.jpg ├── 2-9-http-loc-conf.jpg ├── 2-9-http-main-conf.jpg ├── 2-9-http-merge-conf.jpg ├── 2-9-http-srv-conf.jpg ├── 3-3-server-start.jpg ├── 3-4-follower-processors.png ├── 3-4-leader-processors.png ├── 3-4-zk-leader-election.png ├── 3-5-zk-session-activate.png ├── 3-5-zk-session-bucket.png ├── 3-5-zk-session.png ├── 3-7-create-session.png ├── 3-7-proposal.png ├── 4-1-kafka-framework.jpg ├── 4-2-offset-manager.jpg ├── 4-3-kafka-zk.png ├── 7-btree-index.png ├── 7-btree.jpg ├── 7-index-principle.png ├── 7-innodb-mvcc.png ├── 7-innodb-pkey.png ├── 7-innodb-skey.png ├── 7-myisam-index.png ├── 7-mysql-partition.png ├── 7-mysql-shard-client.png ├── 7-mysql-shard-proxy.png ├── 7-mysql-shard-se.png ├── 7-sql-full-join.jpg ├── 7-sql-isolation.jpg ├── 7-sql-join-summary.jpg ├── 7-sql-join.jpg ├── 7-sql-left-join.jpg ├── 7-sql-order.jpg ├── 7-sql-person.jpg ├── 7-unified-index.webp ├── 7-user-extra.png ├── 7-user-full.png ├── 7-user-split-horizon.png └── 7-user.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf 17 | 18 | # idea project 19 | .idea 20 | -------------------------------------------------------------------------------- /.idea/backend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /2/2.1 启动流程.md: -------------------------------------------------------------------------------- 1 | ## 2.1 启动流程 2 | 3 | Nginx的启动初始化由main函数完成,该函数完成Nginx启动初始化任务,也是所有功能模块的入口。我们看一下该方法: 4 | ```c 5 | int ngx_cdecl main(int argc, char *const *argv) { 6 | ngx_buf_t *b; 7 | ngx_log_t *log; 8 | ngx_uint_t i; 9 | ngx_cycle_t *cycle, init_cycle; 10 | ngx_conf_dump_t *cd; 11 | ngx_core_conf_t *ccf; 12 | ... 13 | 14 | //解析命令行参数 15 | if (ngx_get_options(argc, argv) != NGX_OK) { 16 | return 1; 17 | } 18 | ... 19 | 20 | //初始化并更新时间 21 | ngx_time_init(); 22 | ... 23 | 24 | //获取当前进程的pid,主要用于发送重启、关闭等信号 25 | ngx_pid = ngx_getpid(); 26 | 27 | //初始化日志 28 | log = ngx_log_init(ngx_prefix); 29 | if (log == NULL) { 30 | return 1; 31 | } 32 | ... 33 | 34 | //初始化init_cycle,此时只是简单的分配内存 35 | ngx_memzero(&init_cycle, sizeof(ngx_cycle_t)); 36 | init_cycle.log = log; 37 | ngx_cycle = &init_cycle; 38 | 39 | init_cycle.pool = ngx_create_pool(1024, log); 40 | if (init_cycle.pool == NULL) { 41 | return 1; 42 | } 43 | 44 | //保存命令行参数 45 | if (ngx_save_argv(&init_cycle, argc, argv) != NGX_OK) { 46 | return 1; 47 | } 48 | 49 | //将ngx_get_options方法获取的参数保存到init_cycle中,主要是prefix、conf_prefix、conf_file和conf_param等字段 50 | if (ngx_process_options(&init_cycle) != NGX_OK) { 51 | return 1; 52 | } 53 | 54 | //初始化系统相关变量,如ngx_pagesize、ngx_cacheline_size和ngx_max_sockets等 55 | if (ngx_os_init(log) != NGX_OK) { 56 | return 1; 57 | } 58 | 59 | //初始化一致性Hash表 60 | if (ngx_crc32_table_init函数!= NGX_OK) { 61 | return 1; 62 | } 63 | 64 | ngx_slab_sizes_init(); 65 | 66 | //继承socket套接字,以便热启动时平滑过渡 67 | if (ngx_add_inherited_sockets(&init_cycle) != NGX_OK) { 68 | return 1; 69 | } 70 | 71 | //预初始化模块,主要是进行编号处理 72 | if (ngx_preinit_modules函数!= NGX_OK) { 73 | return 1; 74 | } 75 | 76 | //完成init_cycle的初始化,这个方法很重要,我们会在第二节中相信分析 77 | cycle = ngx_init_cycle(&init_cycle); 78 | if (cycle == NULL) { 79 | if (ngx_test_config) { 80 | ngx_log_stderr(0, "configuration file %s test failed", init_cycle.conf_file.data); 81 | } 82 | 83 | return 1; 84 | } 85 | ... 86 | 87 | //如果有信号,则进入信号处理方法ngx_signal_process 88 | if (ngx_signal) { 89 | return ngx_signal_process(cycle, ngx_signal); 90 | } 91 | 92 | ngx_os_status(cycle->log); 93 | 94 | ngx_cycle = cycle; 95 | 96 | //获取core模块的配置信息 97 | ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); 98 | 99 | if (ccf->master && ngx_process == NGX_PROCESS_SINGLE) { 100 | ngx_process = NGX_PROCESS_MASTER; 101 | } 102 | ... 103 | 104 | //创建进程记录文件 105 | if (ngx_create_pidfile(&ccf->pid, cycle->log) != NGX_OK) { 106 | return 1; 107 | } 108 | ... 109 | 110 | //进入进程处理,一般情况下都是多进程模式 111 | if (ngx_process == NGX_PROCESS_SINGLE) { 112 | ngx_single_process_cycle(cycle); 113 | } else { 114 | ngx_master_process_cycle(cycle); 115 | } 116 | 117 | return 0; 118 | } 119 | ``` 120 | 从该方法中可以看出,启动过程如下所示: 121 | * 调用ngx_get_options函数解析命令参数; 122 | * 调用ngx_time_init函数初始化并更新时间; 123 | * 调用ngx_log_init函数初始化日志; 124 | * 创建全局变量init_cycle的内存池pool; 125 | * 调用ngx_save_argv函数保存命令行参数至全局变量ngx_os_argv、ngx_argc和ngx_argv中; 126 | * 调用ngx_process_options函数初始化init_cycle的prefix、conf_prefix、conf_file和conf_param等字段; 127 | * 调用ngx_os_init函数初始化系统相关变量; 128 | * 调用ngx_crc32_table_init函数初始化CRC表; 129 | * 调用ngx_add_inherited_sockets函数继承sockets; 130 | * 通过环境变量NGINX完成socket的继承,将其保存在全局变量init_cycle的listening数组中; 131 | * 初始化每个模块module的index,并计算ngx_max_module; 132 | * 调用ngx_init_cycle函数进行初始化全局变量init_cycle,这个步骤非常重要; 133 | * 调用ngx_signal_process函数处理进程信号; 134 | * 调用ngx_init_signals函数注册相关信号; 135 | * 若无继承sockets,则调用ngx_daemon函数创建守护进程,并设置其标志; 136 | * 调用ngx_create_pidfile函数创建进程ID记录文件; 137 | * 最后进入进程处理:单进程工作模式或者多进程工作模式 138 | 139 | 其中,这里比较重要的是全局变量cycle的初始化(其中包含模块初始化和配置文件解析)和多进程启动等,接下来我们会详细分析这几个过程。 -------------------------------------------------------------------------------- /2/2.3 模块初始化.md: -------------------------------------------------------------------------------- 1 | ## 2.3 模块初始化 2 | Nginx是高度模块化的,各个功能都会封装在模块中,例如core模块、event模块和http模块等,当然我们也可以自定义模块。本文主要讲解模块的初始化,之后我们将会谈论如何编写Nginx的模块。 3 | 4 | ### 2.3.1 相关数据结构 5 | 6 | #### 模块ngx_module_t 7 | 结构体ngx_module_t主要用于管理每一个模块的详细信息,Nginx的所有模块会放置在全局变量cycle->modules数组中。通过这个数组,我们就可以拿到每个模块的具体信息。我们首先看一下ngx_module_t结构: 8 | ```c 9 | struct ngx_module_s { 10 | ngx_uint_t ctx_index; 11 | ngx_uint_t index; // 模块的唯一标识符号 12 | char *name; // 模块名称 13 | 14 | ngx_uint_t spare0; 15 | ngx_uint_t spare1; 16 | 17 | ngx_uint_t version; // 模块版本 18 | const char *signature; 19 | 20 | void *ctx; // 模块上下文 21 | ngx_command_t *commands; // 模块支持的命令集 22 | ngx_uint_t type; // 模块类型 23 | 24 | ngx_int_t (*init_master)(ngx_log_t *log); //未使用 25 | ngx_int_t (*init_module)(ngx_cycle_t *cycle); //模块初始化的时候调用 26 | ngx_int_t (*init_process)(ngx_cycle_t *cycle); //工作进程初始化时调用 27 | ngx_int_t (*init_thread)(ngx_cycle_t *cycle); //未使用 28 | void (*exit_thread)(ngx_cycle_t *cycle); //未使用 29 | void (*exit_process)(ngx_cycle_t *cycle); //工作进程退出调用 30 | void (*exit_master)(ngx_cycle_t *cycle); //主进程退出时调用 31 | ... 32 | }; 33 | ``` 34 | 这里简要说明一下各字段的含义: 35 | * index:模块索引,前面说过cycle->conf_ctx存储着各个模块的配置文件结构的指针地址,具体到每个模块的配置文件指针地址就是通过这个index来确定的。例如核心模块的配置信息结构是ngx_core_conf_t,而ngx_core_conf_t这个指针就通过该模块的index索引值放在cycle->conf_ctx数组中。 36 | * commands:模块配置命令,Nginx的配置文件都是通过commands命令集来逐个解析具体定义好的配置信息(每个模块不一样),之后我们会详细分析这一块。 37 | * ctx:模块上下文,例如核心模块就是ngx_core_module_t的结构。 38 | * type:模块类型,核心模块的模块类型是NGX_CORE_MODULE。 39 | * init_module:初始化模块的时候会回调的函数。 40 | * init_process:工作进程初始化模块的时候会回调的函数。 41 | 42 | #### ngx_core_module核心模块 43 | nginx.c中定义了核心模块ctx数据结构ngx_core_module_t,核心模块的上下文ngx_core_module_ctx以及定义核心模块。具体如下所示: 44 | ```c 45 | //定义核心模块ctx数据结构,主要定义了创建配置和初始化配置回调函数 46 | typedef struct { 47 | ngx_str_t name; 48 | void *(*create_conf)(ngx_cycle_t *cycle); 49 | char *(*init_conf)(ngx_cycle_t *cycle, void *conf); 50 | } ngx_core_module_t; 51 | 52 | //声明核心模块ctx 53 | static ngx_core_module_t ngx_core_module_ctx = { 54 | ngx_string("core"), 55 | ngx_core_module_create_conf, 56 | ngx_core_module_init_conf 57 | }; 58 | 59 | //声明核心模块 60 | ngx_module_t ngx_core_module = { 61 | NGX_MODULE_V1, 62 | &ngx_core_module_ctx, /* module context */ 63 | ngx_core_commands, /* module directives */ 64 | NGX_CORE_MODULE, /* module type */ 65 | NULL, /* init master */ 66 | NULL, /* init module */ 67 | NULL, /* init process */ 68 | NULL, /* init thread */ 69 | NULL, /* exit thread */ 70 | NULL, /* exit process */ 71 | NULL, /* exit master */ 72 | NGX_MODULE_V1_PADDING 73 | }; 74 | ``` 75 | 76 | ### 2.3.2 模块的初始化 77 | 首先从main方法开始: 78 | ```c 79 | int ngx_cdecl main(int argc, char *const *argv) { 80 | ... 81 | //初始化所有模块 82 | if (ngx_preinit_modules() != NGX_OK) { 83 | return 1; 84 | } 85 | 86 | //初始化全局变量cycle 87 | cycle = ngx_init_cycle(&init_cycle); 88 | ... 89 | } 90 | ``` 91 | 92 | #### 模块编号处理ngx_preinit_modules 93 | 在Nginx启动流程中,会首先对模块进行编号处理。具体代码如下所示: 94 | ```c 95 | //ngx_module.c 96 | //对所有模块进行编号处理,其中ngx_modules数组是在编译时根据configure自动生成的,位于objs/ngx_modules.c文件中 97 | ngx_int_t ngx_preinit_modules(void) { 98 | ngx_uint_t i; 99 | 100 | for (i = 0; ngx_modules[i]; i++) { 101 | ngx_modules[i]->index = i; 102 | ngx_modules[i]->name = ngx_module_names[i]; 103 | } 104 | 105 | ngx_modules_n = i; 106 | ngx_max_module = ngx_modules_n + NGX_MAX_DYNAMIC_MODULES; 107 | 108 | return NGX_OK; 109 | } 110 | ``` 111 | 这里需要说明的时:ngx_modules是一个引用外部的变量,其定义在ngx_modules.h中 112 | ```c 113 | //ngx_modules.h 114 | //模块数组,所有的模块都会保存在此数组中,共有四种类型模块:CORE、CONF、EVENT以及HTTP 115 | extern ngx_module_t *ngx_modules[]; 116 | ``` 117 | 而ngx_modules的模块到底是什么时候确定的呢? 118 | 1. 具体的模块可通过编译前的configure命令进行配置,即设置哪些模块需要编译,哪些不被编译。当编译的时候,会生成ngx_modules.c的文件,里面就包含模块数组。 119 | 2. 新增模块或者减少模块可以在configure命令执行前auto/modules文件里面修改。 120 | 生成的objs/ngx_modules.c文件如下: 121 | ```c 122 | 00001: 123 | 00002: #include 124 | 00003: #include 125 | 00004: 126 | 00005: 127 | 00006: 128 | 00007: extern ngx_module_t ngx_core_module; 129 | 00008: extern ngx_module_t ngx_errlog_module; 130 | 00009: extern ngx_module_t ngx_conf_module; 131 | 00010: extern ngx_module_t ngx_events_module; 132 | 00011: extern ngx_module_t ngx_event_core_module; 133 | 00012: extern ngx_module_t ngx_epoll_module; 134 | 00013: extern ngx_module_t ngx_http_module; 135 | 00014: extern ngx_module_t ngx_http_core_module; 136 | 00015: extern ngx_module_t ngx_http_log_module; 137 | 00016: extern ngx_module_t ngx_http_upstream_module; 138 | 00017: extern ngx_module_t ngx_http_static_module; 139 | 00018: extern ngx_module_t ngx_http_autoindex_module; 140 | 00019: extern ngx_module_t ngx_http_index_module; 141 | 00020: extern ngx_module_t ngx_http_auth_basic_module; 142 | 00021: extern ngx_module_t ngx_http_access_module; 143 | 00022: extern ngx_module_t ngx_http_limit_zone_module; 144 | 00023: extern ngx_module_t ngx_http_limit_req_module; 145 | 00024: extern ngx_module_t ngx_http_geo_module; 146 | 00025: extern ngx_module_t ngx_http_map_module; 147 | 00026: extern ngx_module_t ngx_http_split_clients_module; 148 | 00027: extern ngx_module_t ngx_http_referer_module; 149 | 00028: extern ngx_module_t ngx_http_rewrite_module; 150 | 00029: extern ngx_module_t ngx_http_proxy_module; 151 | ... 152 | ``` 153 | 154 | #### ngx_init_cycle 155 | 继续向下看ngx_init_cycle方法,其中与模块初始化相关代码如下所是: 156 | ```c 157 | ngx_cycle_t * ngx_init_cycle(ngx_cycle_t *old_cycle) { 158 | ... 159 | 160 | //创建模块以及创建模块的配置信息 161 | if (ngx_cycle_modules(cycle) != NGX_OK) { 162 | ngx_destroy_pool(pool); 163 | return NULL; 164 | } 165 | ... 166 | 167 | //调用模块的init_module方法 168 | if (ngx_init_modules(cycle) != NGX_OK) { 169 | exit(1); 170 | } 171 | } 172 | ``` 173 | 174 | #### 初始化cycle->modules 175 | 该方法主要是将原来的全局ngx_modules拷贝到cycle->modules上。具体如下所示: 176 | ```c 177 | //ngx_modules.c 178 | //将静态的全局模块数组拷贝到cycle->modules上 179 | ngx_int_t ngx_cycle_modules(ngx_cycle_t *cycle) { 180 | cycle->modules = ngx_pcalloc(cycle->pool, (ngx_max_module + 1) * sizeof(ngx_module_t *)); 181 | if (cycle->modules == NULL) { 182 | return NGX_ERROR; 183 | } 184 | 185 | ngx_memcpy(cycle->modules, ngx_modules, ngx_modules_n * sizeof(ngx_module_t *)); 186 | cycle->modules_n = ngx_modules_n; 187 | return NGX_OK; 188 | } 189 | ``` 190 | 191 | #### 初始化模块ngx_init_modules 192 | ngx_init_modules方法主要用于每个模块的初始化工作,其主要是调用ngx_module_t结构中定义的init_module回调函数。 193 | ```c 194 | //ngx_modules.c 195 | //初始化模块,调用ngx_module_t中的init_module回调函数 196 | ngx_int_t ngx_init_modules(ngx_cycle_t *cycle) { 197 | ngx_uint_t i; 198 | 199 | for (i = 0; cycle->modules[i]; i++) { 200 | if (cycle->modules[i]->init_module) { 201 | if (cycle->modules[i]->init_module(cycle) != NGX_OK) { 202 | return NGX_ERROR; 203 | } 204 | } 205 | } 206 | return NGX_OK; 207 | } 208 | ``` 209 | 210 | #### 进程初始化init_process 211 | 在第一节中,我们说过启动的最后一步是调用ngx_worker_process_cycle,其会调用ngx_worker_process_init方法,该方法中包含模块的进程初始化,其中会调用模块数据结构中的init_process回调函数。如下所示: 212 | ```c 213 | //回调ngx_module_t结构中的init_process钩子方法 214 | static void ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker) { 215 | ... 216 | for (i = 0; cycle->modules[i]; i++) { 217 | if (cycle->modules[i]->init_process) { 218 | if (cycle->modules[i]->init_process(cycle) == NGX_ERROR) { 219 | exit(2); 220 | } 221 | } 222 | } 223 | ... 224 | } 225 | ``` 226 | 227 | 最后我们看一张模块图: 228 | 229 | ![ngx-conf](../img/2-4-ngx-module-conf.png) 230 | -------------------------------------------------------------------------------- /2/2.6 事件驱动详解.md: -------------------------------------------------------------------------------- 1 | ## 2.6 事件驱动详解 2 | 3 | 由于Nginx采用的是多进程的模式,会存在惊群问题。在Nginx上表现为:Nginx的所有工作进程都要监听socket,当有一个客户端要连到Nginx服务器上,会造成资源的竞争从而造成系统性能下降。 4 | 5 | 我们之前说过,Nginx事件驱动的核心是ngx_process_events_and_timers函数。接下来我们详细看以下该函数,看其如何处理负载均衡、惊群处理和事件分发。具体代码如下所示: 6 | 7 | ```c 8 | void ngx_process_events_and_timers(ngx_cycle_t *cycle) { 9 | ngx_uint_t flags; 10 | ngx_msec_t timer, delta; 11 | 12 | if (ngx_timer_resolution) { 13 | timer = NGX_TIMER_INFINITE; 14 | flags = 0; 15 | } else { 16 | timer = ngx_event_find_timer(); 17 | flags = NGX_UPDATE_TIME; 18 | ... 19 | } 20 | 21 | //ngx_use_accept_mutex变量代表是否使用accept锁,默认true 22 | if (ngx_use_accept_mutex) { 23 | //负载均衡处理 24 | if (ngx_accept_disabled > 0) { 25 | ngx_accept_disabled--; 26 | } else { 27 | //通过竞争锁解决惊群问题 28 | if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { 29 | return; 30 | } 31 | 32 | //成功获取到锁 33 | if (ngx_accept_mutex_held) { 34 | //增加标记NGX_POST_EVENTS,告诉ngx_process_events函数将相关事件分到ngx_posted_accept_events和ngx_posted_events队列。 35 | flags |= NGX_POST_EVENTS; 36 | } else { 37 | //获取锁失败 38 | if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) { 39 | timer = ngx_accept_mutex_delay; 40 | } 41 | } 42 | } 43 | } 44 | 45 | //事件处理函数,并计算执行时消耗的时间 46 | delta = ngx_current_msec; 47 | (void) ngx_process_events(cycle, timer, flags); 48 | delta = ngx_current_msec - delta; 49 | 50 | //处理ngx_posted_accept_events队列中保存的accept事件 51 | ngx_event_process_posted(cycle, &ngx_posted_accept_events); 52 | 53 | //如果拿到锁,处理完accept事件后,则释放锁 54 | if (ngx_accept_mutex_held) { 55 | ngx_shmtx_unlock(&ngx_accept_mutex); 56 | } 57 | 58 | if (delta) { 59 | ngx_event_expire_timers(); 60 | } 61 | 62 | //处理ngx_posted_events队列中保存的普通事件 63 | ngx_event_process_posted(cycle, &ngx_posted_events); 64 | } 65 | ``` 66 | 该函数虽然不长,却有很多比较关键的地方,接下来我们会一一详述: 67 | 68 | 首先我们看一下Nginx如何处理负载均衡: 69 | * 当事件配置初始化的时候,会设置一个全局变量ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n; 70 | * 当ngx_accept_disabled为正数时,就不再处理新的连接accept事件,同时将其减1,直到ngx_accept_disabled降到总连接的7/8以下时,才会调用ngx_trylock_accept_mutex尝试处理新连接事件。 71 | 72 | 接下来我们看一下惊群处理: 73 | * 首先通过ngx_trylock_accept_mutex争抢ngx_accept_mutex,成功获取锁时,才可以处理accept事件。 74 | * 获取锁失败时,接下来调用事件处理函数ngx_process_events时只处理已有连接上的事件,而且是直接调用ev->handler回调函数处理该事件。 75 | * 成功获取锁时,调用ngx_process_events函数时就既需要处理已有连接上的事件,还需要处理新连接上的事件。此时为了防止长时间占用ngx_accept_mutex,ngx_process_events函数在处理事件时会首先将所有事件先放入队列中(NGX_POST_EVENTS标志位),accept事件放入到ngx_posted_accept_events,普通事件放入到ngx_posted_events。接下来会优先处理ngx_posted_accept_events队列中的事件,处理完后立即释放ngx_accept_mutex锁。然后再处理ngx_posted_events队列中的事件。 76 | 77 | 我们详细看一下这个过程: 78 | 79 | ```c 80 | ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle) { 81 | //成功获取锁 82 | if (ngx_shmtx_trylock(&ngx_accept_mutex)) { 83 | //判断是否已经拿到锁 84 | if (ngx_accept_mutex_held && ngx_accept_events == 0) { 85 | return NGX_OK; 86 | } 87 | 88 | //调用ngx_enable_accept_events函数,将监听套接字的读事件添加到当前使用的事件驱动模块中 89 | if (ngx_enable_accept_events(cycle) == NGX_ERROR) { 90 | ngx_shmtx_unlock(&ngx_accept_mutex); 91 | return NGX_ERROR; 92 | } 93 | 94 | ngx_accept_events = 0; 95 | ngx_accept_mutex_held = 1; 96 | 97 | return NGX_OK; 98 | } 99 | 100 | if (ngx_accept_mutex_held) { 101 | //没有拿到锁,调用ngx_disable_accept_events函数,将监听套接字的读事件从当前事件模块中删除 102 | if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) { 103 | return NGX_ERROR; 104 | } 105 | 106 | ngx_accept_mutex_held = 0; 107 | } 108 | 109 | return NGX_OK; 110 | } 111 | 112 | static ngx_int_t ngx_enable_accept_events(ngx_cycle_t *cycle) { 113 | ngx_uint_t i; 114 | ngx_listening_t *ls; 115 | ngx_connection_t *c; 116 | 117 | ls = cycle->listening.elts; 118 | for (i = 0; i < cycle->listening.nelts; i++) { 119 | 120 | c = ls[i].connection; 121 | 122 | if (c == NULL || c->read->active) { 123 | continue; 124 | } 125 | 126 | //添加事件:下文我们会以epoll模块为例详细分析该函数 127 | if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) { 128 | return NGX_ERROR; 129 | } 130 | } 131 | 132 | return NGX_OK; 133 | } 134 | 135 | static ngx_int_t ngx_disable_accept_events(ngx_cycle_t *cycle, ngx_uint_t all) { 136 | ngx_uint_t i; 137 | ngx_listening_t *ls; 138 | ngx_connection_t *c; 139 | 140 | ls = cycle->listening.elts; 141 | for (i = 0; i < cycle->listening.nelts; i++) { 142 | ... 143 | 144 | //删除事件 145 | if (ngx_del_event(c->read, NGX_READ_EVENT, NGX_DISABLE_EVENT) == NGX_ERROR) { 146 | return NGX_ERROR; 147 | } 148 | } 149 | 150 | return NGX_OK; 151 | } 152 | ``` 153 | 该函数比较简单,拿到ngx_accept_mutex,则将监听socket加入到事件分发器(epoll)中;否则从事件分发器删除监听socket。接下来看一下ngx_event_process_posted函数: 154 | 155 | ```c 156 | void ngx_event_process_posted(ngx_cycle_t *cycle, ngx_queue_t *posted) { 157 | ngx_queue_t *q; 158 | ngx_event_t *ev; 159 | 160 | while (!ngx_queue_empty(posted)) { 161 | 162 | q = ngx_queue_head(posted); 163 | ev = ngx_queue_data(q, ngx_event_t, queue); 164 | ngx_delete_posted_event(ev); 165 | 166 | //事件回调函数 167 | ev->handler(ev); 168 | } 169 | } 170 | ``` 171 | 该函数比较简单,就是循环处理ngx_posted_accept_events和ngx_posted_events队列中的相关事件。最后我们看一下事件处理核心函数ngx_process_events:这里以epoll为例,简单看一下ngx_epoll_process_events函数: 172 | 173 | ```c 174 | //ngx_epoll_module.c 175 | static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags) { 176 | int events; 177 | uint32_t revents; 178 | ngx_int_t instance, i; 179 | ngx_uint_t level; 180 | ngx_err_t err; 181 | ngx_event_t *rev, *wev; 182 | ngx_queue_t *queue; 183 | ngx_connection_t *c; 184 | 185 | //调用epoll_wait等待相关事件准备就绪 186 | events = epoll_wait(ep, event_list, (int) nevents, timer); 187 | 188 | err = (events == -1) ? ngx_errno : 0; 189 | 190 | if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) { 191 | ngx_time_update(); 192 | } 193 | ... 194 | 195 | //遍历epoll_wait返回的所有已准备就绪的事件,并处理这些事件 196 | for (i = 0; i < events; i++) { 197 | c = event_list[i].data.ptr; 198 | 199 | instance = (uintptr_t) c & 1; 200 | c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1); 201 | 202 | //获取连接上的读事件 203 | rev = c->read; 204 | //判断事件是否过期 205 | if (c->fd == -1 || rev->instance != instance) { 206 | continue; 207 | } 208 | 209 | revents = event_list[i].events; 210 | //EPOLLERR表示连接出错,EPOLLHUP表示收到RST报文 211 | if (revents & (EPOLLERR|EPOLLHUP)) { 212 | revents |= EPOLLIN|EPOLLOUT; 213 | } 214 | 215 | //连接上活跃的可读事件 216 | if ((revents & EPOLLIN) && rev->active) { 217 | 218 | #if (NGX_HAVE_EPOLLRDHUP) 219 | //EPOLLRDHUP表示对端关闭了读取端 220 | if (revents & EPOLLRDHUP) { 221 | rev->pending_eof = 1; 222 | } 223 | 224 | rev->available = 1; 225 | #endif 226 | 227 | rev->ready = 1; 228 | 229 | if (flags & NGX_POST_EVENTS) { 230 | //拿到ngx_accept_mutex,将accept事件放到ngx_posted_accept_events队列中,普通事件放到ngx_posted_events队列中 231 | queue = rev->accept ? &ngx_posted_accept_events : &ngx_posted_events; 232 | ngx_post_event(rev, queue); 233 | } else { 234 | rev->handler(rev); 235 | } 236 | } 237 | 238 | //获取连接上的写事件 239 | wev = c->write; 240 | //连接上活跃的可写事件 241 | if ((revents & EPOLLOUT) && wev->active) { 242 | //写事件是否过期 243 | if (c->fd == -1 || wev->instance != instance) { 244 | continue; 245 | } 246 | 247 | wev->ready = 1; 248 | if (flags & NGX_POST_EVENTS) { 249 | //拿到ngx_accept_mutex,将普通事件放到ngx_posted_events队列中 250 | ngx_post_event(wev, &ngx_posted_events); 251 | } else { 252 | //否则直接调用handler回调函数处理该事件 253 | wev->handler(wev); 254 | } 255 | } 256 | } 257 | 258 | return NGX_OK; 259 | } 260 | ``` 261 | -------------------------------------------------------------------------------- /2/2.8 Epoll模块详解.md: -------------------------------------------------------------------------------- 1 | ## 2.8 Epoll模块详解 2 | 3 | 之前我们已经介绍了Nginx的事件驱动框架以及件驱动模块的管理。本节基于前面的知识,简单介绍下在Linux系统下的epoll事件驱动模块。关于epoll的使用与原理这里就不再详述,这里主要分析Nginx服务器基于事件驱动框架实现的epoll事件驱动模块。 4 | 5 | ### 2.8.1 Epoll模块数据结构 6 | 7 | 我们首先看一下epoll模块对应的配置项结构体ngx_epoll_conf_t: 8 | ```c 9 | typedef struct { 10 | ngx_uint_t events; //表示epoll_wait函数返回的最大事件数 11 | ngx_uint_t aio_requests; //并发处理异步IO事件个数 12 | } ngx_epoll_conf_t; 13 | ``` 14 | 15 | ### 2.8.2 Epoll模块定义 16 | 17 | 接下来我们看一下epoll模块的定义: 18 | ```c 19 | //epoll模块命令集 20 | static ngx_command_t ngx_epoll_commands[] = { 21 | //该配置项表示epoll_wait函数每次返回的最大事件数 22 | { ngx_string("epoll_events"), 23 | NGX_EVENT_CONF|NGX_CONF_TAKE1, 24 | ngx_conf_set_num_slot, 25 | 0, 26 | offsetof(ngx_epoll_conf_t, events), 27 | NULL }, 28 | 29 | //该配置项表示创建的异步IO上下文能并发处理异步IO事件的个数 30 | { ngx_string("worker_aio_requests"), 31 | NGX_EVENT_CONF|NGX_CONF_TAKE1, 32 | ngx_conf_set_num_slot, 33 | 0, 34 | offsetof(ngx_epoll_conf_t, aio_requests), 35 | NULL }, 36 | 37 | ngx_null_command 38 | }; 39 | 40 | //epoll模块上下文 41 | ngx_event_module_t ngx_epoll_module_ctx = { 42 | &epoll_name, 43 | ngx_epoll_create_conf, /* create configuration */ 44 | ngx_epoll_init_conf, /* init configuration */ 45 | 46 | { 47 | ngx_epoll_add_event, /* add an event */ 48 | ngx_epoll_del_event, /* delete an event */ 49 | ngx_epoll_add_event, /* enable an event */ 50 | ngx_epoll_del_event, /* disable an event */ 51 | ngx_epoll_add_connection, /* add an connection */ 52 | ngx_epoll_del_connection, /* delete an connection */ 53 | NULL, /* process the changes */ 54 | ngx_epoll_process_events, /* process the events */ 55 | ngx_epoll_init, /* init the events */ 56 | ngx_epoll_done, /* done the events */ 57 | } 58 | }; 59 | 60 | //epoll模块定义 61 | ngx_module_t ngx_epoll_module = { 62 | NGX_MODULE_V1, 63 | &ngx_epoll_module_ctx, /* module context */ 64 | ngx_epoll_commands, /* module directives */ 65 | NGX_EVENT_MODULE, /* module type */ 66 | NULL, /* init master */ 67 | NULL, /* init module */ 68 | NULL, /* init process */ 69 | NULL, /* init thread */ 70 | NULL, /* exit thread */ 71 | NULL, /* exit process */ 72 | NULL, /* exit master */ 73 | NGX_MODULE_V1_PADDING 74 | }; 75 | ``` 76 | 在epoll模块定义,我们重点看一下ngx_epoll_module_ctx中的ngx_event_actions_t成员。epoll模块的操作由该成员相关函数实现的。具体如下所示: 77 | ``` 78 | ngx_epoll_add_event, /* add an event */ 79 | ngx_epoll_del_event, /* delete an event */ 80 | ngx_epoll_add_event, /* enable an event */ 81 | ngx_epoll_del_event, /* disable an event */ 82 | ngx_epoll_add_connection, /* add an connection */ 83 | ngx_epoll_del_connection, /* delete an connection */ 84 | NULL, /* process the changes */ 85 | ngx_epoll_process_events, /* process the events */ 86 | ngx_epoll_init, /* init the events */ 87 | ngx_epoll_done, /* done the events */ 88 | ``` 89 | 接下来我们将以该结构体为核心分析epoll模块。 90 | 91 | ### 2.8.3 epoll模块actions分析 92 | 93 | #### epoll模块的事件初始化 94 | epoll模块的事件初始化由ngx_epoll_init函数实现的,该函数主要做了两件事:创建epoll对象和创建event_list数组(调用epoll_wait函数时用于存储从内核复制的已就绪的事件): 95 | ```c 96 | static int ep = -1; //epoll对象描述符 97 | static struct epoll_event *event_list; //作为epoll_wait函数的第二个参数,保存从内存复制的事件 98 | static ngx_uint_t nevents; //epoll_wait函数返回的最多事件数 99 | 100 | static ngx_int_t ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer) { 101 | ngx_epoll_conf_t *epcf; 102 | 103 | //获取epoll模块的配置项结构 104 | epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module); 105 | 106 | if (ep == -1) { 107 | //调用epoll_create函数创建epoll对象 108 | ep = epoll_create(cycle->connection_n / 2); 109 | 110 | if (ep == -1) { 111 | return NGX_ERROR; 112 | } 113 | } 114 | 115 | //预分配epoll事件数组event_list 116 | if (nevents < epcf->events) { 117 | //若现有event_list个数小于配置项所指定的值epcf->events,则先释放,再从新分配; 118 | if (event_list) { 119 | ngx_free(event_list); 120 | } 121 | 122 | //预分配epcf->events个epoll_event结构,并使event_list指向该地址 123 | event_list = ngx_alloc(sizeof(struct epoll_event) * epcf->events, cycle->log); 124 | if (event_list == NULL) { 125 | return NGX_ERROR; 126 | } 127 | } 128 | 129 | //设置正确的epoll_event结构个数 130 | nevents = epcf->events; 131 | 132 | //指定IO的读写方法 133 | ngx_io = ngx_os_io; 134 | 135 | //设置ngx_event_actions接口 136 | ngx_event_actions = ngx_epoll_module_ctx.actions; 137 | 138 | #if (NGX_HAVE_CLEAR_EVENT) 139 | //ET模式 140 | ngx_event_flags = NGX_USE_CLEAR_EVENT 141 | #else 142 | //LT模式 143 | ngx_event_flags = NGX_USE_LEVEL_EVENT 144 | #endif 145 | |NGX_USE_GREEDY_EVENT 146 | |NGX_USE_EPOLL_EVENT; 147 | 148 | return NGX_OK; 149 | } 150 | ``` 151 | 152 | #### epoll模块的事件添加与删除 153 | epoll模块的事件添加与删除分别由ngx_epoll_add_event函数与ngx_epoll_del_event函数实现的,这两个函数都是通过调用epoll_ctl函数向epoll对象添加或者删除事件,所以这里我们先简单看一下epoll_ctl函数的工作原理: 154 | ```c 155 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 156 | /* 157 | * 参数: 158 | * epfd:由epoll_create创建的epoll文件描述符; 159 | * fd:是待操作的文件描述符; 160 | * op:是操作方式,有以下三种操作方式: 161 | * EPOLL_CTL_ADD 将fd注册到epfd中; 162 | * EPOLL_CTL_MOD 修改已在epfd中注册的fd事件; 163 | * EPOLL_CTL_DEL 将fd从epfd中删除; 164 | * 165 | * event:指向struct epoll_event 结构,表示需要监听fd的某种事件; 166 | */ 167 | 168 | typedef union epoll_data { 169 | void *ptr; 170 | int fd; 171 | uint32_t u32; 172 | uint64_t u64; 173 | } epoll_data_t; 174 | 175 | struct epoll_event { 176 | uint32_t events; /* Epoll events */ 177 | epoll_data_t data; /* User data variable */ 178 | }; 179 | 180 | /* 181 | * 其中events有如下的取值: 182 | * EPOLLIN 表示对应的文件描述符可读; 183 | * EPOLLOUT 表示对应的文件描述符可写; 184 | * EPOLLPRI 表示对应的文件描述符有紧急数据可读; 185 | * EPOLLERR 表示对应的文件描述符发生错误; 186 | * EPOLLHUP 表示对应的文件描述符被挂载; 187 | * EPOLLET 表示将EPOLL设置为边缘触发模式(Edge Triggered)。 188 | */ 189 | ``` 190 | 接下来我们以ngx_epoll_add_event为例简单分析一下该过程: 191 | ```c 192 | static ngx_int_t ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags) { 193 | int op; 194 | uint32_t events, prev; 195 | ngx_event_t *e; 196 | ngx_connection_t *c; 197 | struct epoll_event ee; 198 | 199 | //获取连接对象 200 | c = ev->data; 201 | 202 | events = (uint32_t) event; 203 | 204 | //判断待添加的事件是读事件还是写事件 205 | if (event == NGX_READ_EVENT) { 206 | e = c->write; 207 | prev = EPOLLOUT; 208 | } else { 209 | e = c->read; 210 | prev = EPOLLIN|EPOLLRDHUP; 211 | } 212 | 213 | //根据active标志位确定事件是否为活跃事件,以决定是修改还是添加事件 214 | if (e->active) { 215 | op = EPOLL_CTL_MOD; 216 | events |= prev; 217 | } else { 218 | op = EPOLL_CTL_ADD; 219 | } 220 | 221 | //将flags参数加入到events标志位中 222 | ee.events = events | (uint32_t) flags; 223 | //prt存储事件关联的连接对象ngx_connection_t以及过期事件instance标志位 224 | ee.data.ptr = (void *) ((uintptr_t) c | ev->instance); 225 | 226 | //调用epoll_ctl方法向epoll对象添加或者修改事件 227 | if (epoll_ctl(ep, op, c->fd, &ee) == -1) { 228 | return NGX_ERROR; 229 | } 230 | 231 | //将该事件的active标志位设置为1,表示当前事件是活跃事件 232 | ev->active = 1; 233 | return NGX_OK; 234 | } 235 | ``` 236 | 简单说明一下该函数,添加一个事件时,我们要先判断该事件对应的连接是否已经加入了epoll对象中(判断方式是根据该连上的其他事件是否active):如果已经加入了,则应该是通过epoll_ctl函数修改epoll对象上的该连接的事件类型;否则应该是将该连接添加到epoll对象上。 237 | 238 | #### epoll模块的连接添加与删除 239 | epoll模块的连接添加与删除分别由ngx_epoll_add_connection函数与ngx_epoll_del_connection函数实现,这两个函数也是通过调用epoll_ctl函数实现的。这里我们简单看一下ngx_epoll_add_connection函数: 240 | ```c 241 | static ngx_int_t ngx_epoll_add_connection(ngx_connection_t *c) { 242 | struct epoll_event ee; 243 | 244 | //设置事件的类型:可读、可写、ET模式 245 | ee.events = EPOLLIN|EPOLLOUT|EPOLLET|EPOLLRDHUP; 246 | ee.data.ptr = (void *) ((uintptr_t) c | c->read->instance); 247 | 248 | //调用epoll_ctl方法将连接所关联的描述符添加到epoll对象中 249 | if (epoll_ctl(ep, EPOLL_CTL_ADD, c->fd, &ee) == -1) { 250 | return NGX_ERROR; 251 | } 252 | 253 | //设置读写事件的active标志位 254 | c->read->active = 1; 255 | c->write->active = 1; 256 | 257 | return NGX_OK; 258 | } 259 | ``` 260 | 261 | #### epoll模块的事件处理 262 | epoll模块的事件处理由函数ngx_epoll_process_events实现,该函数主要完成事件收集、事件发送的任务。具体代码如下所是: 263 | ```c 264 | static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags) { 265 | int events; 266 | uint32_t revents; 267 | ngx_int_t instance, i; 268 | ngx_uint_t level; 269 | ngx_err_t err; 270 | ngx_event_t *rev, *wev, **queue; 271 | ngx_connection_t *c; 272 | 273 | //调用epoll_wait在规定的timer时间内等待监控的事件准备就绪 274 | events = epoll_wait(ep, event_list, (int) nevents, timer); 275 | 276 | err = (events == -1) ? ngx_errno : 0; 277 | 278 | /* 279 | * 若没有设置timer_resolution配置项,NGX_UPDATE_TIME标志表示每次调用epoll_wait函数返回后需要更新时间; 280 | * 若设置timer_resolution配置项,则每隔timer_resolution配置项参数会设置ngx_event_timer_alarm为1,表示需要更新时间。 281 | */ 282 | //更新时间 283 | if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) { 284 | ngx_time_update(); 285 | } 286 | ... 287 | 288 | //遍历本次epoll_wait返回的所有事件 289 | for (i = 0; i < events; i++) { 290 | //获取与事件关联的连接对象 291 | c = event_list[i].data.ptr; 292 | instance = (uintptr_t) c & 1; 293 | c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1); 294 | 295 | //获取连接上的读事件 296 | rev = c->read; 297 | 298 | //判断该读事件是否过期 299 | if (c->fd == -1 || rev->instance != instance) { 300 | continue; 301 | } 302 | 303 | //获取事件类型 304 | revents = event_list[i].events; 305 | ... 306 | 307 | //连接有可读事件,且该读事件是active活跃的 308 | if ((revents & EPOLLIN) && rev->active) { 309 | 310 | #if (NGX_HAVE_EPOLLRDHUP) 311 | //EPOLLRDHUP表示连接对端关闭了读取端 312 | if (revents & EPOLLRDHUP) { 313 | rev->pending_eof = 1; 314 | } 315 | #endif 316 | 317 | if ((flags & NGX_POST_THREAD_EVENTS) && !rev->accept) { 318 | rev->posted_ready = 1; 319 | } else { 320 | rev->ready = 1; 321 | } 322 | 323 | if (flags & NGX_POST_EVENTS) { 324 | //NGX_POST_EVENTS表示已准备就绪的事件需要延迟处理,根据accept标志位将事件加入到相应的队列中 325 | queue = (ngx_event_t **) (rev->accept ? &ngx_posted_accept_events : &ngx_posted_events); 326 | ngx_locked_post_event(rev, queue); 327 | } else { 328 | //立即调用事件的回调函数处理该事件 329 | rev->handler(rev); 330 | } 331 | } 332 | 333 | //获取连接的写事件,写事件的处理逻辑过程与读事件类似 334 | wev = c->write; 335 | 336 | //连接有可写事件,且该写事件是active活跃的 337 | if ((revents & EPOLLOUT) && wev->active) { 338 | //检查写事件是否过期 339 | if (c->fd == -1 || wev->instance != instance) { 340 | continue; 341 | } 342 | 343 | if (flags & NGX_POST_THREAD_EVENTS) { 344 | wev->posted_ready = 1; 345 | } else { 346 | wev->ready = 1; 347 | } 348 | 349 | if (flags & NGX_POST_EVENTS) { 350 | //NGX_POST_EVENTS表示已准备就绪的事件需要延迟处理 351 | ngx_locked_post_event(wev, &ngx_posted_events); 352 | } else { 353 | //立即调用事件的回调函数处理该事件 354 | wev->handler(wev); 355 | } 356 | } 357 | } 358 | 359 | ngx_mutex_unlock(ngx_posted_events_mutex); 360 | 361 | return NGX_OK; 362 | } 363 | ``` 364 | -------------------------------------------------------------------------------- /3/3.1 ZAB协议详解.md: -------------------------------------------------------------------------------- 1 | ## 3.1 ZAB协议详解 2 | 3 | ### 3.1.1 ZooKeeper简介 4 | ZooKeeper使用了一种称为ZAB(ZooKeeper Atomic Broadcast)的协议作为其一致性的核心。ZAB协议是Paxos协议的一种变形,下面将展示一些协议的核心内容。 5 | 6 | 考虑到Zookeeper主要操作数据的状态,为了保证状态的一致性,Zookeeper提出了两个安全属性: 7 | 1. 全序(Total order):如果消息a在消息b之前发送,则所有Server应该看到相同的结果。 8 | 2. 因果顺序(Causal order):如果消息a在消息b之前发生(a导致了b),并被一起发送,则a始终在b之前被执行。 9 | 10 | 为了保证上述两个安全属性,Zookeeper使用了TCP协议和Leader:通过使用TCP协议保证了消息的全序特性(先发先到);通过Leader解决了因果顺序问题,先到Leader的先执行。 11 | 12 | ### 3.1.2 ZAB协议 13 | ZAB协议包括两种基本的模式:崩溃恢复和消息广播 14 | 1. 崩溃恢复:当整个服务端在启动过程中,或者Leader崩溃退出时,ZAB就会进入恢复模式并选举产生新的Leader服务器。 15 | 2. 当选举产生了新的Leader时,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出崩溃恢复模式,进入消息广播模式。 16 | 17 | 以上大致经历三个步骤:崩溃恢复、数据同步和消息广播。下面具体看看这三个步骤 18 | 19 | #### 崩溃恢复 20 | 启动过程中或者Leader出现网络中断、崩溃退出与重启等异常,将进入恢复模式选举新的Leader服务器,恢复的过程中有2个问题需要解决: 21 | 1. ZAB协议需要确保那些已经在Leader服务器上提交的事务,最终被所有服务器都提交。 22 | 2. ZAB协议需要确保丢弃那些只在Leader服务器上被提交的事务。 23 | 24 | 针对以上两个问题,如果让Leader选举算法能够保证新选出来的Leader服务器拥有集群中所有机器最高编号(ZXID)的Propose,那么就可以保证这个新选出来的Leader一定具有所有已经提交的提案。 25 | 26 | #### 数据同步 27 | Leader服务器会为每个Follower服务器都准备一个队列,并将那些没有被各Follower同步的事务逐个发送给Follower服务器,并在每个消息的后面发送一个commit消息,表示提交事务;等到同步完成之后,leader服务器会将该服务器加入到真正的可用Follower列表中。 28 | 29 | 崩溃恢复中提到2个问题,看看如何解决ZAB协议需要确保丢弃那些只在Leader服务器上被提交的事务:事务编号ZXID被设计为一个64位的数字,低32位是一个简单的递增计数器,高32位是Leader周期的epoch编码,每当选举产生一个新的Leader服务器,就会从这个Leader服务器上取出本地日志中最大事务propose的ZXID,然后解析出epoch,最后对epoch加1;低32位就从0开始重新生成新的ZXID。ZAB协议通过epoch编号来区分Leader周期变化的策略,来保证丢弃那些只在上一个Leader服务器上被提交的事务。 30 | 31 | #### 消息广播 32 | ZAB协议的消息广播过程使用的是一个原子广播协议,类似二阶段提交。 33 | 34 | 客户端的请求,Leader服务器为其生成对应的Propose,并将其发送给其他服务器,然后再分别收集选票,最后进行提交。 35 | 36 | 在广播Propose之前,Leader会为这个Propose分配一个全局唯一的事务ID(即ZXID);由于ZAB协议需要保证每一个消息严格的因果关系,因此必须将每一个Propose按照其ZXID的先后顺序来进行排序与处理。 37 | 38 | 具体做法就是Leader为每一个Follower都各自分配一个单独的队列,然后将需要广播的Propose依次放入队列中。 -------------------------------------------------------------------------------- /3/3.2 Watcher机制.md: -------------------------------------------------------------------------------- 1 | ## 3.2 Watcher机制 2 | -------------------------------------------------------------------------------- /3/3.3 Server启动流程.md: -------------------------------------------------------------------------------- 1 | ## 3.3 Server启动流程 2 | 3 | ZooKeeper服务端有两种启动模式:单机版和集群版,这里以集群版为例简单分析一下该流程。具体如下图所示: 4 | ![server-start](../img/3-3-server-start.jpg) 5 | 6 | ### 3.3.1 预启动 7 | 我们从QuorumPeerMain的main方法开始: 8 | ```Java 9 | public static void main(String[] args) { 10 | QuorumPeerMain main = new QuorumPeerMain(); 11 | try { 12 | main.initializeAndRun(args); 13 | } catch (Exception e) { 14 | ... 15 | System.exit(1); 16 | } 17 | System.exit(0); 18 | } 19 | protected void initializeAndRun(String[] args) throws ConfigException, IOException { 20 | //解析配置 21 | QuorumPeerConfig config = new QuorumPeerConfig(); 22 | if (args.length == 1) { 23 | config.parse(args[0]); 24 | } 25 | 26 | //启动purge task定期清理快照文件 27 | DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config 28 | .getDataDir(), config.getDataLogDir(), config 29 | .getSnapRetainCount(), config.getPurgeInterval()); 30 | purgeMgr.start(); 31 | 32 | if (args.length == 1 && config.servers.size() > 0) { 33 | //run as quorum 34 | runFromConfig(config); 35 | } else { 36 | //run as standalone 37 | ZooKeeperServerMain.main(args); 38 | } 39 | } 40 | ``` 41 | 预启动步骤如下: 42 | 1. 解析配置文件zoo.cfg 43 | 2. 创建并启动历史文件清理器DatadirCleanupManager 44 | 3. 判断当前是集群模式还是单机模式的启动 45 | 46 | ### 3.3.2 初始化 47 | 我们看一下初始化流程: 48 | ```Java 49 | public void runFromConfig(QuorumPeerConfig config) throws IOException { 50 | ... 51 | try { 52 | //创建ServerCnxnFactory 53 | ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory(); 54 | cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns()); 55 | 56 | //初始化QuorumPeer 57 | quorumPeer = new QuorumPeer(); 58 | quorumPeer.setClientPortAddress(config.getClientPortAddress()); 59 | //创建数据管理器FileTxnSnaplog 60 | quorumPeer.setTxnFactory(new FileTxnSnapLog(new File(config.getDataLogDir()), new File(config.getDataDir()))); 61 | quorumPeer.setQuorumPeers(config.getServers()); 62 | quorumPeer.setElectionType(config.getElectionAlg()); 63 | quorumPeer.setMyid(config.getServerId()); 64 | quorumPeer.setTickTime(config.getTickTime()); 65 | quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout()); 66 | quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout()); 67 | quorumPeer.setInitLimit(config.getInitLimit()); 68 | quorumPeer.setSyncLimit(config.getSyncLimit()); 69 | quorumPeer.setQuorumVerifier(config.getQuorumVerifier()); 70 | quorumPeer.setCnxnFactory(cnxnFactory); 71 | //创建内存数据库ZKDatabase 72 | quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory())); 73 | quorumPeer.setLearnerType(config.getPeerType()); 74 | quorumPeer.setSyncEnabled(config.getSyncEnabled()); 75 | quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs()); 76 | 77 | quorumPeer.start(); 78 | quorumPeer.join(); 79 | } catch (InterruptedException e) { 80 | ... 81 | } 82 | } 83 | //QuorumPeer.Java 84 | public synchronized void start() { 85 | //恢复本地数据 86 | loadDataBase(); 87 | //启动ServerCnxnFactory 88 | cnxnFactory.start(); 89 | //开始Leader选举 90 | startLeaderElection(); 91 | //启动QuorumPeer线程 92 | super.start(); 93 | } 94 | ``` 95 | 初始化步骤如下: 96 | 1. 创建并初始化ServerCnxnFactory 97 | 2. 创建并初始化QuorumPeer实例 98 | 3. 创建ZooKeeper数据管理器FileTxnSnapLog 99 | 4. 创建内存数据库ZKDatabase 100 | 5. 恢复本地数据 101 | 6. 启动ServerCnxnFactory主线程 102 | 7. 开始Leader选举 103 | 8. 启动QuorumPeer线程 104 | 105 | ### 3.3.3 运行 106 | 接下来我们看一下QuorumPeer的run方法: 107 | ```Java 108 | public void run() { 109 | ... 110 | try { 111 | //Main loop 112 | while (running) { 113 | switch (getPeerState()) { 114 | case LOOKING: 115 | if (Boolean.getBoolean("readonlymode.enabled")) { 116 | // Create read-only server but don't start it immediately 117 | final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer( 118 | logFactory, this, 119 | new ZooKeeperServer.BasicDataTreeBuilder(), 120 | this.zkDb); 121 | 122 | Thread roZkMgr = new Thread() { 123 | public void run() { 124 | try { 125 | // lower-bound grace period to 2 secs 126 | sleep(Math.max(2000, tickTime)); 127 | if (QuorumPeer.ServerState.LOOKING.equals(getPeerState())) { 128 | roZk.startup(); 129 | } 130 | } catch (Exception e) { 131 | ... 132 | } 133 | } 134 | }; 135 | try { 136 | roZkMgr.start(); 137 | setBCVote(null); 138 | setCurrentVote(makeLEStrategy().lookForLeader()); 139 | } catch (Exception e) { 140 | LOG.warn("Unexpected exception", e); 141 | setPeerState(QuorumPeer.ServerState.LOOKING); 142 | } finally { 143 | roZkMgr.interrupt(); 144 | roZk.shutdown(); 145 | } 146 | } else { 147 | try { 148 | setBCVote(null); 149 | setCurrentVote(makeLEStrategy().lookForLeader()); 150 | } catch (Exception e) { 151 | setPeerState(QuorumPeer.ServerState.LOOKING); 152 | } 153 | } 154 | break; 155 | case OBSERVING: 156 | try { 157 | setObserver(makeObserver(logFactory)); 158 | observer.observeLeader(); 159 | } finally { 160 | observer.shutdown(); 161 | setObserver(null); 162 | setPeerState(QuorumPeer.ServerState.LOOKING); 163 | } 164 | break; 165 | case FOLLOWING: 166 | try { 167 | setFollower(makeFollower(logFactory)); 168 | follower.followLeader(); 169 | } finally { 170 | follower.shutdown(); 171 | setFollower(null); 172 | setPeerState(QuorumPeer.ServerState.LOOKING); 173 | } 174 | break; 175 | case LEADING: 176 | try { 177 | setLeader(makeLeader(logFactory)); 178 | leader.lead(); 179 | setLeader(null); 180 | } finally { 181 | if (leader != null) { 182 | leader.shutdown("Forcing shutdown"); 183 | setLeader(null); 184 | } 185 | setPeerState(QuorumPeer.ServerState.LOOKING); 186 | } 187 | break; 188 | } 189 | } 190 | } finally { 191 | ... 192 | } 193 | } 194 | ``` 195 | 根据当前节点的状态来启动不同的流程: 196 | * 如果是Looking状态,则调用FastLeaderElection::lookForLeader来发起选举流程 197 | * 如果是OBSERVING状态,则开始Observer流程 198 | * 如果是FOLLOWING状态,则开始Follower流程 199 | * 如果是LEADING状态,则开始Leader流程 200 | -------------------------------------------------------------------------------- /3/3.5 Leader选举.md: -------------------------------------------------------------------------------- 1 | ## 3.4 Leader选举 2 | 3 | ### 3.4.1 Leader选举流程 4 | 前面我们说过,ZK的Leader选举依赖于ZXID(事务ID)和SID(服务器ID)。这里我们首先介绍一下服务器启动时期的Leader选举: 5 | * 设置状态为LOOKING,初始化投票Vote(sid, zxid),并将其广播到集群其它节点,节点首次投票都是选举自己作为Leader,将自身的服务SID、处理的最近一个事务请求的ZXID及当前状态封装到Vote中将其广播出去,然后进入循环等待及处理其它节点的投票信息的流程中。 6 | * 循环等待流程中,节点每收到一个外部的Vote选票,都需要将其与自己的Vote选票进行PK,规则为取ZXID大的,若ZXID相等,则取SID大的那个投票。若外部投票胜选,节点需要将该选票覆盖之前的Vote选票,并将其再次广播出去;同时还要统计是否有过半的选票,无则继续循环等待新的投票,有则说明已经选出Leader,选举结束退出循环,根据选举结果及各自角色切换状态:Leader切换成LEADING,follower切换到FOLLOWING和observer切换到OBSERVING状态。 7 | 8 | 9 | ### 3.4.2 QuorumCnxManager详解 10 | 11 | 12 | ### 3.4.2 FastLeaderElection内部类 13 | FastLeaderElection有三个较为重要的内部类,分别为Notification、ToSend和Messenger。接下来我们一一看一下这三个类: 14 | #### Notification 15 | ```Java 16 | static public class Notification { 17 | public final static int CURRENTVERSION = 0x1; 18 | 19 | int version; 20 | //proposed leader 21 | long leader; 22 | 23 | //zxid of the proposed leader 24 | long zxid; 25 | 26 | //epoch 27 | long electionEpoch; 28 | 29 | //current state of sender 30 | QuorumPeer.ServerState state; 31 | 32 | //address of sender 33 | long sid; 34 | 35 | //epoch of the proposed leader 36 | long peerEpoch; 37 | } 38 | 39 | ``` 40 | Notification表示收到的选举投票信息(其他服务器发来的选举投票信息),其包含了被选举者的id、zxid、选举周期等信息。 41 | 42 | #### ToSend 43 | ToSend表示发送给其他服务器的选举投票信息,也包含了被选举者的id、zxid、选举周期等信息。 44 | 45 | #### Messenger类 46 | Messenger包含了WorkerReceiver和WorkerSender两个内部类。接下来我们看一下这两个类: 47 | 48 | WorkerReceiver是选票接收器,其实现了Runnable接口,其会不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue中。其主要逻辑在run方法中,我们详细看一下该方法: 49 | ```Java 50 | public void run() { 51 | Message response; 52 | while (!stop) { 53 | try{ 54 | response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS); 55 | if(response == null) continue; 56 | 57 | //当前的投票者集合不包含目标服务器 58 | if(!self.getVotingView().containsKey(response.sid)){ 59 | Vote current = self.getCurrentVote(); 60 | ToSend notmsg = new ToSend(ToSend.mType.notification, 61 | current.getId(), 62 | current.getZxid(), 63 | logicalclock, 64 | self.getPeerState(), 65 | response.sid, 66 | current.getPeerEpoch()); 67 | 68 | sendqueue.offer(notmsg); 69 | } else { 70 | // We check for 28 bytes for backward compatibility 71 | if (response.buffer.capacity() < 28) { 72 | continue; 73 | } 74 | boolean backCompatibility = (response.buffer.capacity() == 28); 75 | response.buffer.clear(); 76 | 77 | //构造选票信息Notification 78 | Notification n = new Notification(); 79 | 80 | // State of peer that sent this message 81 | QuorumPeer.ServerState ackstate = QuorumPeer.ServerState.LOOKING; 82 | switch (response.buffer.getInt()) { 83 | case 0: 84 | ackstate = QuorumPeer.ServerState.LOOKING; 85 | break; 86 | case 1: 87 | ackstate = QuorumPeer.ServerState.FOLLOWING; 88 | break; 89 | case 2: 90 | ackstate = QuorumPeer.ServerState.LEADING; 91 | break; 92 | case 3: 93 | ackstate = QuorumPeer.ServerState.OBSERVING; 94 | break; 95 | default: 96 | continue; 97 | } 98 | 99 | n.leader = response.buffer.getLong(); 100 | n.zxid = response.buffer.getLong(); 101 | n.electionEpoch = response.buffer.getLong(); 102 | n.state = ackstate; 103 | n.sid = response.sid; 104 | if(!backCompatibility){ 105 | n.peerEpoch = response.buffer.getLong(); 106 | } else { 107 | n.peerEpoch = ZxidUtils.getEpochFromZxid(n.zxid); 108 | } 109 | 110 | //Version added in 3.4.6 111 | n.version = (response.buffer.remaining() >= 4) ? response.buffer.getInt() : 0x0; 112 | 113 | //当前Server是否是Looking状态 114 | if(self.getPeerState() == QuorumPeer.ServerState.LOOKING){ 115 | recvqueue.offer(n); 116 | if((ackstate == QuorumPeer.ServerState.LOOKING) && (n.electionEpoch < logicalclock)){ 117 | Vote v = getVote(); 118 | ToSend notmsg = new ToSend(ToSend.mType.notification, 119 | v.getId(), 120 | v.getZxid(), 121 | logicalclock, 122 | self.getPeerState(), 123 | response.sid, 124 | v.getPeerEpoch()); 125 | sendqueue.offer(notmsg); 126 | } 127 | } else { 128 | /* 129 | * If this server is not looking, but the one that sent the ack 130 | * is looking, then send back what it believes to be the leader. 131 | */ 132 | Vote current = self.getCurrentVote(); 133 | if(ackstate == QuorumPeer.ServerState.LOOKING){ 134 | ToSend notmsg; 135 | if(n.version > 0x0) { 136 | notmsg = new ToSend( 137 | ToSend.mType.notification, 138 | current.getId(), 139 | current.getZxid(), 140 | current.getElectionEpoch(), 141 | self.getPeerState(), 142 | response.sid, 143 | current.getPeerEpoch()); 144 | } else { 145 | Vote bcVote = self.getBCVote(); 146 | notmsg = new ToSend( 147 | ToSend.mType.notification, 148 | bcVote.getId(), 149 | bcVote.getZxid(), 150 | bcVote.getElectionEpoch(), 151 | self.getPeerState(), 152 | response.sid, 153 | bcVote.getPeerEpoch()); 154 | } 155 | sendqueue.offer(notmsg); 156 | } 157 | } 158 | } 159 | } catch (InterruptedException e) { 160 | ... 161 | } 162 | } 163 | } 164 | ``` 165 | 简单看一下该方法: 166 | * 其首先会从QuorumCnxManager中的recvQueue队列中取出其他服务器发来的选举消息Message; 167 | * 然后判断该消息中的服务器id是否包含在可以投票的服务器集合中:若不是,则会将本服务器的投票发送给该服务器; 168 | * 否则则解析出该服务器的投票Notification,然后判断当前服务器是否为LOOKING状态:若为LOOKING,则直接将该选票放入到FastLeaderElection中的recvqueue成员中,并发出自己的投票信息;否则则判断投票服务器是否为LOOKING状态,如果是则将leader信息发送给该服务器。 169 | 170 | WorkerSender类是选票发送器,其也实现了Runnable接口。其会不断地从sendqueue中获取待发送的选票,并通过QuorumCnxManager中的toSend方法发送出去。其相关逻辑比较简单,这里就不再详述。 171 | 172 | ### 3.4.3 FastLeaderElection详解 173 | FastLeaderElection实现了Election接口,其需要实现接口中定义的lookForLeader方法,其是标准的Fast Paxos算法的实现,各服务器之间基于TCP协议进行选举。我们直接看其核心方法lookForLeader,该方法用于开始新一轮的Leader选举,具体如下所示: 174 | ```Java 175 | public Vote lookForLeader() throws InterruptedException { 176 | ... 177 | try { 178 | HashMap recvset = new HashMap(); 179 | HashMap outofelection = new HashMap(); 180 | 181 | int notTimeout = finalizeWait; 182 | 183 | synchronized(this){ 184 | //更新logicalclock 185 | logicalclock++; 186 | //更新proposal 187 | updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); 188 | } 189 | 190 | //发送自己的选票 191 | sendNotifications(); 192 | 193 | //Loop in which we exchange notifications until we find a leader 194 | while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){ 195 | //从recvqueue中获取投票信息 196 | Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS); 197 | 198 | //如果没有获取到选票,则继续发送自己的投票 199 | if(n == null){ 200 | if(manager.haveDelivered()){ 201 | sendNotifications(); 202 | } else { 203 | manager.connectAll(); 204 | } 205 | 206 | //Exponential backoff 207 | int tmpTimeOut = notTimeout*2; 208 | notTimeout = (tmpTimeOut < maxNotificationInterval? tmpTimeOut : maxNotificationInterval); 209 | } else if(self.getVotingView().containsKey(n.sid)) { 210 | //投票中的sid必须在投票服务器集合中 211 | switch (n.state) { 212 | case LOOKING: 213 | if (n.electionEpoch > logicalclock) { 214 | //更新logicalclock,然后更新proposal并重新发出投票 215 | logicalclock = n.electionEpoch; 216 | recvset.clear(); 217 | if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) { 218 | updateProposal(n.leader, n.zxid, n.peerEpoch); 219 | } else { 220 | updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); 221 | } 222 | sendNotifications(); 223 | } else if (n.electionEpoch < logicalclock) { 224 | break; 225 | } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) { 226 | //选票PK,更新proposal并重新发出投票 227 | updateProposal(n.leader, n.zxid, n.peerEpoch); 228 | sendNotifications(); 229 | } 230 | 231 | //recvset用于记录当前选举过程中收到的所有投票 232 | recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); 233 | 234 | //检查是否已经选出leader 235 | if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock, proposedEpoch))) { 236 | //再次确认leader 237 | //Verify if there is any change in the proposed leader 238 | while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) { 239 | if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) { 240 | recvqueue.put(n); 241 | break; 242 | } 243 | } 244 | 245 | //表示之前选出的leader就是最优的 246 | if (n == null) { 247 | self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState()); 248 | 249 | Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock, proposedEpoch); 250 | leaveInstance(endVote); 251 | return endVote; 252 | } 253 | } 254 | break; 255 | case OBSERVING: 256 | break; 257 | case FOLLOWING: 258 | case LEADING: 259 | //Consider all notifications from the same epoch together. 260 | if(n.electionEpoch == logicalclock){ 261 | recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); 262 | 263 | //已经完成了leader选举 264 | if(ooePredicate(recvset, outofelection, n)) { 265 | self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState()); 266 | 267 | Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch); 268 | //清空recvqueue队列 269 | leaveInstance(endVote); 270 | return endVote; 271 | } 272 | } 273 | 274 | //Before joining an established ensemble, verify a majority is following the same leader. 275 | outofelection.put(n.sid, new Vote(n.version, 276 | n.leader, 277 | n.zxid, 278 | n.electionEpoch, 279 | n.peerEpoch, 280 | n.state)); 281 | 282 | if(ooePredicate(outofelection, outofelection, n)) { 283 | synchronized(this) { 284 | logicalclock = n.electionEpoch; 285 | self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState()); 286 | } 287 | Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch); 288 | leaveInstance(endVote); 289 | return endVote; 290 | } 291 | break; 292 | default: 293 | break; 294 | } 295 | } 296 | } 297 | return null; 298 | } finally { 299 | ... 300 | } 301 | } 302 | ``` 303 | 简单分析一下该方法: 304 | * 该方法用于开始新一轮的Leader选举,因此其会首先自增logicalclock,然后更新自己的选票信息,最后调用sendNotifications方法向其他服务器发送自己的选票信息; 305 | * 然后会不断的从recvqueue队列中不断的获取外部选票,如果无法获取选票,则确认自己是否和集群中其他服务器是否保持连接,并再次发送自己的选票; 306 | * 获取到外部选票后,然后开始处理外部选票: 307 | * 如果该选票的服务器状态处于Looking状态时,则首先根据logicalclock处理选票,决定是否要更新自己的logicalclock和选票信息;然后对收到的选票信息进行归档处理:将收到的外部选票放到recvset中,然后统计选票,看是否已经选出了leader,如果已经选出了leader,则再进行一次确认,看是否有更优的选票产生; 308 | * 如果处于Following或者Leading状态时,说明此时不在选举期间,则设置本服务器的状态,清理recvqueue等状态,退出选举过程。 309 | 310 | ### 3.5.5 总结 311 | 最后我们再来总结一下ZK的leader选举过程: 312 | 313 | #### 选举的时机 314 | 首先我们看一下ZK的选举时机: 315 | * 节点启动时:每个节点启动的时候状态默认都是LOOKING,接下来就是要进行选主了。 316 | * Leader节点异常:正常运行时Leader节点会周期性地向Follower发送心跳信息(称之为ping),如果一个Follower未收到Leader节点的心跳信息,Follower节点的状态会从FOLLOWING转变为LOOKING,然后进入选主阶段。 317 | * 多数Follower节点异常:Leader节点也会检测Follower节点的状态,如果多数Follower节点不再响应Leader节点(可能是Leader节点与Follower节点之间产生了网络分区),那么Leader节点可能此时也不再是合法的Leader了,也必须要进行一次新的选主。 318 | 319 | #### 处理选票 320 | 然后我们再回顾一下一个Server接收到选票的处理流程,即上面WorkReceiver的逻辑: 321 | 当一个Server收到其他Server发过来的选票时,首先判断当前服务器是否为LOOKING状态: 322 | * 若为LOOKING,则直接将该选票放入到FastLeaderElection中的recvqueue成员中,并发出自己的投票信息,进入选主流程; 323 | * 否则则判断投票服务器是否为LOOKING状态,如果是则将leader信息发送给该服务器。 324 | 325 | #### 选主流程 326 | 接下来我们就来详细说明一下ZK的选主流程: 327 | * serverA首先自增electionEpoch,然后为自己投票:serverA会首先从日志中加载数据,从而得到lastProcessedZxid,然后初始投票Vote的内容:`sid, zxid, peerEpoch`,并发送出去; 328 | * serverB接收到上述通知,然后进行投票PK(具体逻辑参见上面的lookForLeader方法); 329 | * 根据server的状态来判定leader:如果发来的投票的server的状态是LOOKING状态,说明处于选举阶段,则判断recvset中是否有选票过半,如果过半了则说明leader选举成功了,如果当前server的id等于上述过半投票的proposedLeader,则说明自己将成为了leader,否则自己将成为了follower;如果当前发来的投票的server的状态是FOLLOWING、LEADING状态,则说明leader选举过程已经完成了,则发过来的投票就是leader的信息,这里就需要判断发过来的投票是否在recvset或者outofelection中是否过半,同时还要检查leader是否给自己发送过投票信息,从投票信息中确认该leader是不是LEADING状态。 330 | 331 | 最后一个检查的原因如下: 332 | ZK中leader和follower都是各自检测是否进入leader选举过程。leader检测到未过半的server的ping回复,则leader会进入LOOKING状态,但是follower有自己的检测,感知这一事件,还需要一定时间,在此期间,如果其他server加入到该集群,可能会收到其他follower的过半的对之前leader的投票,但是此时该leader已经不处于LEADING状态了,所以需要这么一个检查来排除这种情况。 333 | -------------------------------------------------------------------------------- /3/3.6 会话管理.md: -------------------------------------------------------------------------------- 1 | ## 3.5 会话管理 2 | 3 | ### 3.5.1 概述 4 | 会话(Session)是ZooKeeper中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话息息相关,这其中就包括临时节点的生命周期、客户端请求的顺序执行以及Watcher通知机制等。在Java中,ZooKeeper的连接与会话就是客户端通过实例化ZooKeeper对象来实现客户端与服务器创建并保持TCP连接的过程。 5 | 6 | #### 会话状态 7 | 在ZooKeeper客户端与服务端成功完成连接创建后,就建立了一个会话。ZooKeeper会话在整个生命周期中,会在不同的会话状态之间进行切换,这些状态一般可以分为五种,如下: 8 | * CONNECTING 9 | * CONNECTED 10 | * RECONNECTING 11 | * RECONNECTED 12 | * CLOSE 13 | 14 | 一旦客户端开始创建ZooKeeper对象,那么客户端状态就会变成CONNECTING,同时开始连接服务器,成功连接上服务器后,客户端状态将变更为CONNECTED。当客户端与服务器之间的连接会出现断开时,客户端会自动进行重连操作,此时其状态再次变为CONNECTING,直到重新连接上服务器后,客户端状态又会再次转变成CONNECTED。因此,通常情况下,在ZooKeeper运行期间,客户端的状态总是介于CONNECTING和CONNECTED两者之一。因此在使用ZooKeeper时注意避免在connecting时对zookeeper进行读写。 15 | 16 | #### Session 17 | Session是ZooKeeper中的会话实体,代表了一个客户端会话。其包含以下4个基本属性: 18 | * sessionID :会话ID,用来唯一标识一个会话,每次客户端创建新会话的时候,ZooKeeper都会为其分配一个全局唯一的sessionID 19 | * TimeOut :会话超时时间。客户端在构造ZooKeeper实例的时候,会配置一个sessionTimeout参数用于指定会话的超时时间。ZooKeeper客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定会话的超时时间 20 | * TickTime :下次会话超时时间点。为了便于ZooKeeper对会话实行“分桶策略”管理,一种高效的会话的超时检查与清理策略,ZooKeeper会为每个会话标记一个下次会话超时时间点。TickTime是一个13位的long型数据,其值接近于当前时间加上TimeOut,但不完全相等 21 | * isClosing :该属性用于标记一个会话是否已经被关闭。通常当服务端检测到一个会话已经超时失效的时候,会将该会话的isClosing属性标记为“已关闭”,这样就能确保不再处理来自该会话的新请求了 22 | 23 | sessionID用来唯一标识一个会话,因此ZooKeeper必须保证sessionID的全局唯一性。在每次客户端向服务端发起“会话创建”请求时,服务端都会为其分配一个sessionID,现在我们就来看看sessionID究竟是如何生成的。在SessionTracker初始化的时候,会调用initializeNextSession方法来生成一个初始化的sessionID,具体代码如下所示: 24 | ```Java 25 | public static long initializeNextSession(long id) { 26 | long nextSid = 0; 27 | nextSid = (System.currentTimeMillis() << 24) >> 8; 28 | nextSid = nextSid | (id <<56); 29 | return nextSid; 30 | } 31 | ``` 32 | 其中的id表示配置在myid文件中的值,通常是一个整数,如1、2、3。该算法的高8位确定了所在机器,后56位使用当前时间的毫秒表示进行随机。 33 | 34 | #### SessionTracker 35 | SessionTracker是ZooKeeper服务端的会话管理器,负责会话的创建、管理和清理等工作。可以说,整个会话的生命周期都离不开SessionTracker的管理。每一个会话在SessionTracker内部都保留了三份,具体如下: 36 | * sessionsById:这是一个`HashMap`类型的数据结构,用于根据sessionID来管理Session实体 37 | * sessionsWithTimeout:这是一个`ConcurrentHashMaps`类型的数据结构,用于根据sessionID来管理会话的超时时间。该数据结构和ZooKeeper内存数据库相连通,会被定期持久化到快照文件中去 38 | * sessionSets:这是一个`HashMap`类型的数据结构,用于根据下次会话超时时间点来归档会话,便于进行会话管理和超时检査 39 | 40 | ### 3.5.2 会话创建 41 | 下一节我们会详细分析一下ZooKeeper服务端如何处理客户端的“会话创建”请求,这里我们只简单看一下相关方法: 42 | ```Java 43 | public void processConnectRequest(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException { 44 | BinaryInputArchive bia = BinaryInputArchive.getArchive(new ByteBufferInputStream(incomingBuffer)); 45 | //解码ConnectRequest 46 | ConnectRequest connReq = new ConnectRequest(); 47 | connReq.deserialize(bia, "connect"); 48 | 49 | //判读是否是readOnly客户端 50 | boolean readOnly = false; 51 | try { 52 | readOnly = bia.readBool("readOnly"); 53 | cnxn.isOldClient = false; 54 | } catch (IOException e) { 55 | ... 56 | } 57 | if (readOnly == false && this instanceof ReadOnlyZooKeeperServer) { 58 | throw new CloseRequestException(msg); 59 | } 60 | 61 | //检查客户端zxid 62 | if (connReq.getLastZxidSeen() > zkDb.dataTree.lastProcessedZxid) { 63 | throw new CloseRequestException(msg); 64 | } 65 | //协商sessionTimeout 66 | int sessionTimeout = connReq.getTimeOut(); 67 | byte passwd[] = connReq.getPasswd(); 68 | int minSessionTimeout = getMinSessionTimeout(); 69 | if (sessionTimeout < minSessionTimeout) { 70 | sessionTimeout = minSessionTimeout; 71 | } 72 | int maxSessionTimeout = getMaxSessionTimeout(); 73 | if (sessionTimeout > maxSessionTimeout) { 74 | sessionTimeout = maxSessionTimeout; 75 | } 76 | cnxn.setSessionTimeout(sessionTimeout); 77 | 78 | // We don't want to receive any packets until we are sure that the session is setup 79 | cnxn.disableRecv(); 80 | 81 | //判断是否需要重新创建会话 82 | long sessionId = connReq.getSessionId(); 83 | if (sessionId != 0) { 84 | long clientSessionId = connReq.getSessionId(); 85 | serverCnxnFactory.closeSession(sessionId); 86 | cnxn.setSessionId(sessionId); 87 | reopenSession(cnxn, sessionId, passwd, sessionTimeout); 88 | } else { 89 | createSession(cnxn, passwd, sessionTimeout); 90 | } 91 | } 92 | long createSession(ServerCnxn cnxn, byte passwd[], int timeout) { 93 | //向SessionTracker注册该会话,并生成sessionID 94 | long sessionId = sessionTracker.createSession(timeout); 95 | //生成会话密钥 96 | Random r = new Random(sessionId ^ superSecret); 97 | r.nextBytes(passwd); 98 | ByteBuffer to = ByteBuffer.allocate(4); 99 | to.putInt(timeout); 100 | cnxn.setSessionId(sessionId); 101 | //接下来将该请求交给Processor处理 102 | submitRequest(cnxn, sessionId, OpCode.createSession, 0, to, null); 103 | return sessionId; 104 | } 105 | //SessionTrackerImpl.Java 106 | synchronized public long createSession(int sessionTimeout) { 107 | addSession(nextSessionId, sessionTimeout); 108 | return nextSessionId++; 109 | } 110 | synchronized public void addSession(long id, int sessionTimeout) { 111 | //注册session 112 | sessionsWithTimeout.put(id, sessionTimeout); 113 | if (sessionsById.get(id) == null) { 114 | SessionImpl s = new SessionImpl(id, sessionTimeout, 0); 115 | sessionsById.put(id, s); 116 | } 117 | //激活session 118 | touchSession(id, sessionTimeout); 119 | } 120 | synchronized public boolean touchSession(long sessionId, int timeout) { 121 | SessionImpl s = sessionsById.get(sessionId); 122 | // Return false, if the session doesn't exists or marked as closing 123 | if (s == null || s.isClosing()) { 124 | return false; 125 | } 126 | long expireTime = roundToInterval(System.currentTimeMillis() + timeout); 127 | if (s.tickTime >= expireTime) { 128 | // Nothing needs to be done 129 | return true; 130 | } 131 | SessionSet set = sessionSets.get(s.tickTime); 132 | if (set != null) { 133 | set.sessions.remove(s); 134 | } 135 | s.tickTime = expireTime; 136 | set = sessionSets.get(s.tickTime); 137 | if (set == null) { 138 | set = new SessionSet(); 139 | sessionSets.put(expireTime, set); 140 | } 141 | set.sessions.add(s); 142 | return true; 143 | } 144 | ``` 145 | 简单总结一下:在ZooKeeper服务端,首先将会由NIOServerCnxn来负责接收来自客户端的“会话创建”请求,并反序列化出ConnectRequest请求,然后协商超时时间,生成sessionID和会话密钥,并将其注册到sessionsById和sessionsWithTimeout中去,同时进行会话的激活。然后该“会话请求”还会在ZooKeeper服务端的各个请求处理器之间进行顺序流转,最终完成会话的创建。 146 | 147 | ### 3.5.3 会话管理 148 | 接下来我们看一下,ZooKeeper服务端是如何管理这些会话的。 149 | 150 | #### 分桶策略 151 | ZooKeeper的会话管理主要是由SessionTracker负责的,其采用了一种特殊的会话管理方式,我们称之为“分桶策略”。所谓分桶策略,是指将类似的会话放在同一区块中进行管理,以便于ZooKeeper对会话进行不同区块的隔离处理以及同一区块的统一处理,如下图所示。 152 | 153 | ![zk-session-bucket](../img/3-5-zk-session-bucket.png) 154 | 155 | 在上图中,我们可以看到,ZooKeeper将所有的会话都分配在了不同的区块之中,分配的原则是每个会话的“下次超时时间点”(ExpirationTime)。ExpirationTime是指该会话最近一次可能超时的时间点,对于一个新创建的会话而言,其会话创建完毕后,ZooKeeper就会为其计算ExpirationTime,其计算公式如下: 156 | ``` 157 | ExpirationTime_ = CurrentTime + SessionTimeout 158 | ExpirationTime = (ExpirationTime_/ExpirationInterval +1) x Expirationlnterval 159 | ``` 160 | Leader服务器会定时地进行会话超时检查,其时间间隔是ExpirationInterval,默认值是tickTime的值(2000ms)。为了方便对多个会话同时进行超时检查,所以ExpirationTime总是ExpirationInterval的整数倍。 161 | 162 | #### 会话激活 163 | 为了保持会话的有效性,客户端会在会话超时时间内向服务端发送Ping请求来激活该会话,服务端收到该请求后执行touchSession方法来激活该会话,相关代码如下所示: 164 | ```Java 165 | synchronized public boolean touchSession(long sessionId, int timeout) { 166 | SessionImpl s = sessionsById.get(sessionId); 167 | //检查会话是否已经关闭 168 | if (s == null || s.isClosing()) { 169 | return false; 170 | } 171 | //计算新的超时时间 172 | long expireTime = roundToInterval(System.currentTimeMillis() + timeout); 173 | if (s.tickTime >= expireTime) { 174 | return true; 175 | } 176 | //迁移会话 177 | SessionSet set = sessionSets.get(s.tickTime); 178 | if (set != null) { 179 | set.sessions.remove(s); 180 | } 181 | s.tickTime = expireTime; 182 | set = sessionSets.get(s.tickTime); 183 | if (set == null) { 184 | set = new SessionSet(); 185 | sessionSets.put(expireTime, set); 186 | } 187 | set.sessions.add(s); 188 | return true; 189 | } 190 | ``` 191 | 简单说明一下该方法: 192 | * 检验该会话是否已经被关闭:如果该会话已经被关闭,那么不再继续激活该会话。 193 | * 计算该会话新的超时时间ExpirationTime_New。 194 | * 迁移会话:将该会话从老的区块中取出,放入ExpirationTime_New对应的新区块中,如图下图所示: 195 | 196 | ![zk-session-activate](../img/3-5-zk-session-activate.png) 197 | 198 | #### 会话超时检查 199 | 在ZooKeeper中,会话超时检查同样是由SessionTracker负责的。SessionTracker中有一个单独的线程专门进行会话超时检查,其工作机制非常简单:逐个依次地对会话桶中剩下的会话进行检查清理。具体代码如下所示: 200 | ```Java 201 | synchronized public void run() { 202 | try { 203 | while (running) { 204 | currentTime = System.currentTimeMillis(); 205 | if (nextExpirationTime > currentTime) { 206 | this.wait(nextExpirationTime - currentTime); 207 | continue; 208 | } 209 | SessionSet set; 210 | set = sessionSets.remove(nextExpirationTime); 211 | if (set != null) { 212 | for (SessionImpl s : set.sessions) { 213 | //标记会话已关闭 214 | setSessionClosing(s.sessionId); 215 | //清理该会话 216 | expirer.expire(s); 217 | } 218 | } 219 | nextExpirationTime += expirationInterval; 220 | } 221 | } catch (InterruptedException e) { 222 | handleException(this.getName(), e); 223 | } 224 | } 225 | ``` 226 | 简单说明一下该方法:由于SessionTracker是以ExpirationInterval的倍数作为时间点来分布会话的,因此只需要在这些指定的时间点上进行检查即可,这样大大提高了会话检查的效率。 227 | 228 | ### 3.5.4 会话清理 229 | 当SessionTracker的会话超时检查线程整理出一些已经过期的会话后,那么就要开始进行会话清理了。看一下相关代码: 230 | ```Java 231 | public void expire(Session session) { 232 | long sessionId = session.getSessionId(); 233 | close(sessionId); 234 | } 235 | private void close(long sessionId) { 236 | submitRequest(null, sessionId, OpCode.closeSession, 0, null, null); 237 | } 238 | ``` 239 | 这里以发起closeSession请求的方式关闭会话,我们之后会详述ZooKeeper事务请求处理的流程。这里只简单说明一下会话清理的相关工作:会话清理主要是需要清理相关的临时节点,然后将该会话从SessionTracker中删除,最后关闭对应的NIOServerCnxn连接。 240 | 241 | ### 3.5.5 重连与异常处理 242 | 在ZooKeeper中,客户端与服务端之间维持的是一个长连接,在sessionTimeout时间内,服务端会不断地检测该客户端是否还处于正常连接——服务端会将客户端的每次操作视为一次有效的心跳检测来反复地进行会话激活。因此在正常情况下,客户端会话是一直有效的。然而当客户端与服务端之间的连接断开后,用户在客户端可能主要会看到两类异常:CONNECTION_LOSS和SESSION_EXPIRED。那么该如何正确处理CONNECTION_LOSS和SESSION_EXPIRED呢? 243 | 244 | #### 连接断开:CONNECTION_LOSS 245 | 有时会因为网络问题导致客户端与服务器断开连接,即CONNECTION_LOSS。在这种情况下,ZooKeeper客户端会自动从地址列表中选取新的地址并尝试重连,直到最终成功连接上服务器。那么如果客户端在进行数据读写时,正好出现了CONNECTION_LOSS,那么客户端会立即收到None-Disconnected通知,同时会抛出ConnectionLossException异常。在这种情况下,我们需要做的是catch住该异常,然后等待ZooKeeper的客户端自动完成重连,重连成功后客户端会收到None-SyncConnected通知,之后重试刚才的操作即可(由于ZooKeeper的操作是幂等的,所以重试不会带来问题)。 246 | 247 | #### 会话失效:SESSION_EXPIRED 248 | SESSION_EXPIRED是指会话过期,通常发生在CONNECTION_LOSS期间。客户端和服务器连接断幵之后,由于重连耗时过长,超过了会话超时时间(sessionTimeout),那么服务器认为这个会话已经结束了,就会开始进行会话清理。但是另一方面,该客户端本身不知道会话已经失效,并且其客户端状态还是DISCONNECTED。之后,如果客户端重新连接上了服务器,服务器会告诉客户端该会话已经失效(SESSION_EXPIRED)。在这种情况下,应用需要重新实例化一个ZooKeeper对象,并且根据情况决定是否需要恢复临时数据。 249 | 250 | #### 会话转移:SESSION_MOVED 251 | 会话转移是指客户端会话从一台服务器转移到了另一台服务器上。正如上文中提到,假设客户端C1和服务器S1之间的连接断开后,如果通过尝试重连后,成功连接上了新的服务器S2并且延续了有效会话,那么就可以认为会话从S1转移到了S2上。 252 | 253 | 会话转移会导致请求会被覆盖,假设我们的ZooKeeper服务器集群有三台机器:S1、S2和S3。在开始的时候,客户端C1与服务器S1建立连接且维持着正常的会话,某一个时刻,C1向服务器发送了一个请求R1:`setData ('/$7_4_4/session_moved', 1)`,但是在请求发送到服务器之前,客户端和服务器恰好发生了连接断开,并且在很短的时间内重新连接上了服务器S2。之后,C1又向服务器S2发送了一个请求R2:`setData ('/$7_4_4/session_moved', 2)`。这个时候,S2能够正确地处理请求R2,但是请求R1最终也到达了服务器S1,S1处理了请求R1,于是,对于客户端C1来说,它的第2次请求R2就被请求R1覆盖了。 254 | 255 | 当然,上面这个问题非常罕见,只有在C1和S1之间的网路非常慢的情况下才会发生,一旦发生这个问题,将会产生非常严重的后果。 256 | 257 | 因此,在3.2.0版本之后,ZooKeeper明确提出了会话转移的概念,同时封装了SessionMovedException异常。之后,在处理客户端请求的时候,会首先检查会话的所有者(Owner),如果客户端请求的会话Owner不是当前服务器的话,那么就会直接抛出SessionMovedException异常。当然,由于客户端已经和这个服务器断开了连接,因此无法收到这个异常。只有多个客户端使用相同的sessionId/sessionPasswd创建会话时,才会收到这样的异常。因为一旦有一个客户端会话创建成功,那么ZooKeeper服务器就会认为该sessionId对应的那个会话已经发生了转移,于是,等到第二个客户端连接上服务器后,就被认为是“会话转移”的情况了。 258 | -------------------------------------------------------------------------------- /3/3.7 事务请求处理.md: -------------------------------------------------------------------------------- 1 | ## 3.7 事务请求处理 2 | 3 | 本节我们以会话创建请求为例看一下ZooKeeper服务端对于事务请求的处理。其大体可以分为六个环节: 4 | * 请求接收 5 | * 会话创建 6 | * 预处理 7 | * 事务处理 8 | * 事务应用 9 | * 会话响应 10 | 11 | 其大体流程如下图所示: 12 | ![create-session](../img/3-7-create-session.png) 13 | 14 | #### 请求接收 15 | 前面我们说过,NIOServerCnxnFactory主要负责Server和Client之间的交互,我们看一下其run方法: 16 | ```Java 17 | public void run() { 18 | while (!ss.socket().isClosed()) { 19 | try { 20 | selector.select(1000); 21 | Set selected; 22 | synchronized (this) { 23 | selected = selector.selectedKeys(); 24 | } 25 | ArrayList selectedList = new ArrayList(selected); 26 | Collections.shuffle(selectedList); 27 | for (SelectionKey k : selectedList) { 28 | if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) { 29 | //处理客户端连接 30 | SocketChannel sc = ((ServerSocketChannel) k.channel()).accept(); 31 | InetAddress ia = sc.socket().getInetAddress(); 32 | int cnxncount = getClientCnxnCount(ia); 33 | if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns){ 34 | sc.close(); 35 | } else { 36 | sc.configureBlocking(false); 37 | SelectionKey sk = sc.register(selector, SelectionKey.OP_READ); 38 | NIOServerCnxn cnxn = createConnection(sc, sk); 39 | sk.attach(cnxn); 40 | addCnxn(cnxn); 41 | } 42 | } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) { 43 | //处理客户端请求 44 | NIOServerCnxn c = (NIOServerCnxn) k.attachment(); 45 | c.doIO(k); 46 | } 47 | } 48 | selected.clear(); 49 | } catch (RuntimeException e) { 50 | ... 51 | } catch (Exception e) { 52 | ... 53 | } 54 | } 55 | closeAll(); 56 | } 57 | ``` 58 | 当客户端连接请求到达时,首先调用createConnection方法创建连接NIOServerCnxn,然后调用addCnxn方法将该连接保存到cnxns中。接下来我们看一下Server端如何处理客户端数据请求doIO方法,我们看一下其readPayload方法: 59 | ```Java 60 | private void readPayload() throws IOException, InterruptedException { 61 | ... 62 | if (incomingBuffer.remaining() == 0) { // have we read length bytes? 63 | packetReceived(); 64 | incomingBuffer.flip(); 65 | if (!initialized) { 66 | //处理会话连接请求 67 | readConnectRequest(); 68 | } else { 69 | readRequest(); 70 | } 71 | lenBuffer.clear(); 72 | incomingBuffer = lenBuffer; 73 | } 74 | } 75 | 76 | private void readConnectRequest() throws IOException, InterruptedException { 77 | if (zkServer == null) { 78 | throw new IOException("ZooKeeperServer not running"); 79 | } 80 | zkServer.processConnectRequest(this, incomingBuffer); 81 | initialized = true; 82 | } 83 | 84 | public void processConnectRequest(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException { 85 | BinaryInputArchive bia = BinaryInputArchive.getArchive(new ByteBufferInputStream(incomingBuffer)); 86 | //解码ConnectRequest 87 | ConnectRequest connReq = new ConnectRequest(); 88 | connReq.deserialize(bia, "connect"); 89 | 90 | //判读是否是readOnly客户端 91 | boolean readOnly = false; 92 | try { 93 | readOnly = bia.readBool("readOnly"); 94 | cnxn.isOldClient = false; 95 | } catch (IOException e) { 96 | ... 97 | } 98 | if (readOnly == false && this instanceof ReadOnlyZooKeeperServer) { 99 | throw new CloseRequestException(msg); 100 | } 101 | 102 | //检查客户端zxid 103 | if (connReq.getLastZxidSeen() > zkDb.dataTree.lastProcessedZxid) { 104 | throw new CloseRequestException(msg); 105 | } 106 | //协商sessionTimeout 107 | int sessionTimeout = connReq.getTimeOut(); 108 | byte passwd[] = connReq.getPasswd(); 109 | int minSessionTimeout = getMinSessionTimeout(); 110 | if (sessionTimeout < minSessionTimeout) { 111 | sessionTimeout = minSessionTimeout; 112 | } 113 | int maxSessionTimeout = getMaxSessionTimeout(); 114 | if (sessionTimeout > maxSessionTimeout) { 115 | sessionTimeout = maxSessionTimeout; 116 | } 117 | cnxn.setSessionTimeout(sessionTimeout); 118 | 119 | // We don't want to receive any packets until we are sure that the 120 | // session is setup 121 | cnxn.disableRecv(); 122 | 123 | //判断是否需要重新创建回话 124 | long sessionId = connReq.getSessionId(); 125 | if (sessionId != 0) { 126 | long clientSessionId = connReq.getSessionId(); 127 | serverCnxnFactory.closeSession(sessionId); 128 | cnxn.setSessionId(sessionId); 129 | reopenSession(cnxn, sessionId, passwd, sessionTimeout); 130 | } else { 131 | createSession(cnxn, passwd, sessionTimeout); 132 | } 133 | } 134 | ``` 135 | 再简单回顾一下请求接收的流程: 136 | 1. I/O层接收来自客户端的请求:在ZooKeeper中,NIOServerCnxn实例维护每一个客户端连接,其负责统一接收来自客户端的所有请求,并将请求内容从底层网络I/O中完整地读取出来。 137 | 2. 判断是否是客户端“会话创建”请求。 138 | 3. 反序列化ConnectRequest请求。 139 | 4. 判断是否是Readonly客户端:在ZooKeeper中,如果当前ZooKeeper服务器是以Readonly模式启动的,那么所有来自非Readonly型客户端的请求将无法被处理。 140 | 5. 检查客户端ZXID:在正常情况下,服务端的ZXID大于客户端的ZXID,因此如果发现客户端的ZXID值大于服务端的ZXID值,那么服务端将不接受该客户端的“会话创建”请求。 141 | 6. 协商sessionTimeout。 142 | 7. 判断是否需要重新创建会话:服务端根据客户端请求中是否包含sessionID来判断该客户端是否需要重新创建会话。 143 | 144 | #### 会话创建 145 | 接下来我们看一下会话创建流程,从createSession方法开始: 146 | ```Java 147 | long createSession(ServerCnxn cnxn, byte passwd[], int timeout) { 148 | //向SessionTracker注册该会话,并生成sessionID 149 | long sessionId = sessionTracker.createSession(timeout); 150 | //生成会话密钥 151 | Random r = new Random(sessionId ^ superSecret); 152 | r.nextBytes(passwd); 153 | ByteBuffer to = ByteBuffer.allocate(4); 154 | to.putInt(timeout); 155 | cnxn.setSessionId(sessionId); 156 | //接下来将该请求交给Processor处理 157 | submitRequest(cnxn, sessionId, OpCode.createSession, 0, to, null); 158 | return sessionId; 159 | } 160 | //SessionTrackerImpl.Java 161 | synchronized public long createSession(int sessionTimeout) { 162 | addSession(nextSessionId, sessionTimeout); 163 | return nextSessionId++; 164 | } 165 | synchronized public void addSession(long id, int sessionTimeout) { 166 | //注册session 167 | sessionsWithTimeout.put(id, sessionTimeout); 168 | if (sessionsById.get(id) == null) { 169 | SessionImpl s = new SessionImpl(id, sessionTimeout, 0); 170 | sessionsById.put(id, s); 171 | } 172 | //激活session 173 | touchSession(id, sessionTimeout); 174 | } 175 | synchronized public boolean touchSession(long sessionId, int timeout) { 176 | SessionImpl s = sessionsById.get(sessionId); 177 | // Return false, if the session doesn't exists or marked as closing 178 | if (s == null || s.isClosing()) { 179 | return false; 180 | } 181 | long expireTime = roundToInterval(System.currentTimeMillis() + timeout); 182 | if (s.tickTime >= expireTime) { 183 | // Nothing needs to be done 184 | return true; 185 | } 186 | SessionSet set = sessionSets.get(s.tickTime); 187 | if (set != null) { 188 | set.sessions.remove(s); 189 | } 190 | s.tickTime = expireTime; 191 | set = sessionSets.get(s.tickTime); 192 | if (set == null) { 193 | set = new SessionSet(); 194 | sessionSets.put(expireTime, set); 195 | } 196 | set.sessions.add(s); 197 | return true; 198 | } 199 | ``` 200 | 简单回顾一下会话创建的流程: 201 | 1. 为客户端生成sessionID:每个ZooKeeper服务器在启动的时候,都会初始化一个会话管理器(SessionTracker),同时初始化sessionID,我们将其称为“基准sessionID”。因此针对每个客户端,只需要在这个“基准sessionID”的基础上进行逐个递增就可以了。 202 | 2. 注册会话:创建会话最重要的工作就是向SessionTracker中注册该会话。SessionTracker中维护了两个比较重要的数据结构,分别是sessionsWithTimeout和sessionsById。前者根据sessionID保存了所有会话的超时时间,而后者则是根据sessionID保存了所有会话实体。 203 | 3. 激活会话:向SessionTracker注册完会话后,接下来还需要对会话进行激活操作。激活会话过程涉及ZooKeeper会话管理的分桶策略,激活会话的核心是为会话安排一个区块,以便会话清理程序能够快速高效地进行会话清理。 204 | 4. 生成会话密码:服务端在创建一个客户端会话的时候,会同时为客户端生成一个会话密码,连同sessionID一起发送给客户端,作为会话在集群中不同机器间转移的凭证。 205 | 206 | #### Proccessor处理 207 | 接下来我们看一下Processor对请求的处理,前面我们说过Leader中第一个Processor是PrepRequestProcessor,而Follower中第一个Processor是FollowerRequestProcessor,由于会话创建请求是事务请求,该Processor会将其转交给Leader处理。所以我们这里简单看一下RrepReqeustProcessor的处理逻辑: 208 | 1. 创建请求事务头:对于事务请求,ZooKeeper首先会为其创建请求事务头,服务端后续的请求处理器都基于该请求头来识别当前请求是否是事务请求。请求事务头包含了一个事务请求最基本的一些信息,包括sessionID、ZXID、CXID和请求类型等。 209 | 2. 创建请求事务体:对于事务请求,ZooKeeper还会为其创建请求的事务体。 210 | 3. 注册与激活会话:此处的注册与激活会话过程,和上面的注册激活流程是重复的,不过并不会引起其他的问题。此处进行会话注册与激活的目的是处理由Follower服务器转发过来的会话创建请求。在这种情况下,其实尚未在Leader的SessionTracker中进行会话的注册,因此需要在此处进行一次注册与激活。 211 | 212 | 完成对请求的预处理后,PrepRequestProcessor处理器会将请求交付给自己的下一级处理器:ProposalRequestProcessor。从ProposalRequestProcessor处理器开始,事务请求的处理将会进入三个子处理流程,分别是Sync流程、Proposal流程和Commit流程。如下图所示: 213 | 214 | ![proposal](../img/3-7-proposal.png) 215 | 216 | ##### Sync流程 217 | 所谓Sync流程,其核心就是使用SyncRequestProcessor处理器记录事务日志的过程。ProposalRequestProcessor处理器在接收到一个上级处理器流转过来的请求后,首先会判断该请求是否是事务请求,如果是事务清求,则会通过事务日志的形式将其记录下来。Leader服务器和Follower服务器的请求处理链路中都会有这个处理器,两者在事务日志的记录功能上是完全一致的。 218 | 219 | 完成事务日志记录后,每个服务器都会向Leader服务器发送ACK消息(Leader是通过AckRequestProcessor,而Follower服务器是通过SendAckRequestProcessor),表明自身完成了事务日志的记录,以便Leader服务器统计每个事务请求的投票情况。 220 | 221 | ##### Proposal流程 222 | 在ZooKeeper的实现中,每一个事务请求都需要集群中过半机器投票认可才能被真正应用到ZooKeeper的内存数据库中去,这个投票与统计过程被称为“Proposal流程”。该流程具体过程如下所示: 223 | 1. 发起投票:如果当前请求是事务请求,那么Leader服务器就会发起一轮事务投票。在发起事务投票之前,首先会检查当前服务端的ZXID是否可用。 224 | 2. 生成提议Proposal:如果当前服务端的ZXID可用,那么就可以开始事务投票了。ZooKeeper会将之前创建的请求头和事务体,以及ZXID和请求本身序列化到Proposal对象中。 225 | 3. 广播提议:生成Proposal后,Leader服务器会以ZXID作为标识,将该提议放入投票箱outstandingProposals中,同时会将该提议广播给所有的Follower服务器。 226 | 4. 收集ACK反馈:Follower服务器在接收到Leader发来的这个提议后,会进入Sync流程来进行事务日志的记录,一旦日志记录完成后,就会发送ACK消息给Leader服务器,Leader服务器根据这些ACK消息来统计每个提议的投票情况。当一个提议获得了集群中过半机器的投票,那么就认为该提议通过,接下去就可以进入提议的Commit阶段了。 227 | 5. 将请求放入toBeApplied队列:在该提议被提交之前,ZooKeeper首先会将其放入toBeApplied队列中去。 228 | 6. 广播COMMIT消息:一旦ZooKeeper确认一个提议已经可以被提交了,那么Leader服务器就会向Follower和Observer服务器发送COMMIT消息,以便所有服务器都能够提交该提议。这里需要注意的是,由于Observer服务器并未参加之前的提议投票,因此Observer服务器尚未保存任何关于该提议的信息,所以在广播COMMIT消息的时候,需要区别对待,Leader会向其发送一种被称为“INTORM”的消息,该消息体中包含了当前提议的内容。而对于Follower服务器,由于已经保存了所有关于该提议的信息,因此Leader服务器只需要向其发送ZXID即可。 229 | 230 | ##### Commit流程 231 | 1. 将请求交付给CommitProcessor处理器:CommitProcessor处理器在收到请求后,并不会立即处理,而是会将其放入queuedRequests队列中。 232 | 2. 处理queuedRequests队列请求:CommitProcessor处理器会有一个单独的线程来处理从上一级处理器流转下来的请求。当检测到queuedRequests队列中已经有新的请求进来,就会逐个从队列中取出请求进行处理。 233 | 3. 标记nextPending:如果从queuedRequests队列中取出的请求是一个事务请求,那么就需要进行集群中各服务器之间的投票处理,同时需要将nextPending标记为当前请求。标记nextPending的作用,一方面是为了确保事务请求的顺序性,另一方面也是便于CommitProcessor处理器检测当前集群中是否正在进行事务请求的投票。 234 | 4. 等待Proposal投票:在Commit流程处理的同时,Leader已经根据当前事务请求生成了一个提议Proposal,并广播给了所有的Follower服务器。因此,此时Commit流程需要等待,直到投票结束。 235 | 5. 投票通过:如果一个提议已经获得了过半机器的投票认可,那么将会进入请求提交阶段。ZooKeeper会将该请求放入committedRequests队列中,同时唤醒Commit流程。 236 | 6. 提交请求:一旦发现committedRequests队列中已经有可以提交的请求了,那么Commit流程就会开始提交请求。当然在提交以前,为了保证事务请求的顺序执行,Commit流程还会对比之前标记的nextPending和committedRequests队列中第一个请求是否一致。如果检查通过,那么Commit流程就会将该请求放入toProcess队列中,然后交付给下一个请求处理器:FinalRequestProcessor。 237 | 238 | FinalRequestProcessor处理器会首先检查outstandingChanges队列中请求的有效性,如果发现这些请求已经落后于当前正在处理的请求,那么直接从outstandingChanges队列中移除。在之前的请求链中,我们仅仅是将该事务请求记录到了事务日志中去,而内存数据库中的状态尚未变更。因此在这个环节,我们需要将事务变更应用到内存数据库中。但对于“会话创建”这类事务请求,ZooKeeper做了特殊处理。因为在ZooKeeper内存中,会话的管理都是由SessionTracker负责的,而在会话创建的步骤9中,ZooKeeper已经将会话信息注册到了SessionTracker中,因此此处无须对内存数据库做任何处理,只需要再次向SessionTracker进行会话注册即可。一旦完成事务请求的内存数据库应用,就可以将该请求放入commitProposal队列中。commitProposal队列用来保存最近被提交的事务请求,以便集群间机器进行数据的快速同步。 239 | 240 | #### 会话响应 241 | 客户端请求在经过ZooKeeper服务端处理链路的所有请求处理器的处理后,就进入最后的会话响应阶段了。会话响应阶段非常简单,这里就不再详述。 -------------------------------------------------------------------------------- /3/3.8 数据与存储.md: -------------------------------------------------------------------------------- 1 | ## 3.8 数据与存储 2 | -------------------------------------------------------------------------------- /4/4.1 Kafka简介.md: -------------------------------------------------------------------------------- 1 | ## 4.1 Kafka简介 2 | 3 | ### 4.1.1 Kafka简介 4 | Kafka是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下: 5 | 1. 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能 6 | 2. 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条以上消息的传输 7 | 3. 支持Kafka Server间的消息分区,及分布式消费,同时保证每个Partition内的消息顺序传输 8 | 4. 同时支持离线数据处理和实时数据处理 9 | 5. Scale out:支持在线水平扩展 10 | 11 | ### 4.1.2 Kafka基本组成 12 | 在Kafka集群中Producer将消息发送给以Topic命名的消息队列中,Consumer订阅 13 | Broker 14 | Kafka集群包含一个或多个服务器,这种服务器被称为broker 15 | Topic 16 | 每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处) 17 | Partition 18 | Partition是物理上的概念,每个Topic包含一个或多个Partition 19 | Producer 20 | 负责发布消息到Kafka broker 21 | Consumer 22 | 消息消费者,向Kafka broker读取消息的客户端 23 | Consumer Group 24 | 每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group) 25 | 26 | ### 4.1.2 Kafka拓扑结构 27 | 一个典型的Kafka集群中包含若干Producer,若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。 28 | 29 | ![kafka-framework](../img/4-1-kafka-framework.jpg) 30 | 31 | 一个简单的消息发送流程: 32 | 1. Producer根据指定的路由方法(Round-Robin、Hash等),将消息push到Topic的某个Partition里; 33 | 2. Broker接收到Producer发过来的消息后,将其持久化到硬盘,并保留消息指定时长,而不关注消息是否被消费; 34 | 3. Consumer从Broker中pull消息,并控制获取消息的offset。 35 | 36 | #### Topic&Partition 37 | Topic在逻辑上可以被认为是一个queue,每条消费都必须指定它的Topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。 38 | 39 | 每个日志文件都是一个log entry序列,每个log entry包含一个4字节整型数值(值为N+5),1个字节的”magic value”,4个字节的CRC校验码,其后跟N个字节的消息体。每条消息都有一个当前Partition下唯一的64字节的offset,它指明了这条消息的起始位置。磁盘上存储的消息格式如下: 40 | message length : 4 bytes (value: 1+4+n) 41 | “magic” value : 1 byte 42 | crc : 4 bytes 43 | payload : n bytes 44 | 这个log entry并非由一个文件构成,而是分成多个segment,每个segment以该segment第一条消息的offset命名并以“.kafka”为后缀。另外会有一个索引文件,它标明了每个segment下包含的log entry的offset范围,如下图所示。 45 | 46 | 因为每条消息都被append到该Partition中,属于顺序写磁盘,因此效率非常高,这是Kafka高吞吐率的一个很重要的保证。一般message queue而会删除已经被消费的消息,而Kafka集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要),因此Kafka提供两种策略删除旧数据。一是基于时间,二是基于Partition文件大小。例如可以通过配置$KAFKA_HOME/config/server.properties,让Kafka删除一周前的数据,也可在Partition文件超过1GB时删除旧数据,配置如下所示: 47 | ``` 48 | # The minimum age of a log file to be eligible for deletion 49 | log.retention.hours=168 50 | # The maximum size of a log segment file. When this size is reached a new log segment will be created. 51 | log.segment.bytes=1073741824 52 | # The interval at which log segments are checked to see if they can be deleted according to the retention policies 53 | log.retention.check.interval.ms=300000 54 | # If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction. 55 | log.cleaner.enable=false 56 | ``` 57 | 这里要注意,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高Kafka性能无关。选择怎样的删除策略只与磁盘以及具体的需求有关。另外,Kafka会为每一个Consumer Group保留一些metadata信息——当前消费的消息的position,也即offset。这个offset由Consumer控制。正常情况下Consumer会在消费完一条消息后递增该offset。当然,Consumer也可将offset设成一个较小的值,重新消费一些消息。因为offet由Consumer控制,所以Kafka broker是无状态的,它不需要标记哪些消息被哪些消费过,也不需要通过broker去保证同一个Consumer Group只有一个Consumer能消费某一条消息,因此也就不需要锁机制,这也为Kafka的高吞吐率提供了有力保障。 58 | 59 | ### 4.1.3 Producer消息路由 60 | Producer发送消息到broker时,会根据Partition机制选择将其存储到哪一个Partition。如果Partition机制设置合理,所有消息可以均匀分布到不同的Partition里,这样就实现了负载均衡。如果一个Topic对应一个文件,那这个文件所在的机器I/O将会成为这个Topic的性能瓶颈,而有了Partition后,不同的消息可以并行写入不同broker的不同Partition里,极大的提高了吞吐率。可以在$KAFKA_HOME/config/server.properties中通过配置项num.partitions来指定新建Topic的默认Partition数量,也可在创建Topic时通过参数指定,同时也可以在Topic创建之后通过Kafka提供的工具修改。 61 |    62 | 在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Partition。 63 | 64 | ### 4.1.4 Consumer 65 | #### Low Level API 66 | 67 | #### High Level API 68 | 使用Consumer high level API时,同一Topic的一条消息只能被同一个Consumer Group内的一个Consumer消费,但多个Consumer Group可同时消费这一消息。这是Kafka用来实现一个Topic消息的广播(发给所有的Consumer)和单播(发给某一个Consumer)的手段。一个Topic可以对应多个Consumer Group。如果需要实现广播,只要每个Consumer有一个独立的Group就可以了。要实现单播只要所有的Consumer在同一个Group里。用Consumer Group还可以将Consumer进行自由的分组而不需要多次发送消息到不同的Topic。 69 | 70 | 实际上,Kafka的设计理念之一就是同时提供离线处理和实时处理。根据这一特性,可以使用Storm这种实时流处理系统对消息进行实时在线处理,同时使用Hadoop这种批处理系统进行离线处理,还可以同时将数据实时备份到另一个数据中心,只需要保证这三个操作所使用的Consumer属于不同的Consumer Group即可。下图是Kafka在Linkedin的一种简化部署示意图。 71 | 72 | 下面这个例子更清晰地展示了Kafka Consumer Group的特性。首先创建一个Topic (名为topic1,包含3个Partition),然后创建一个属于group1的Consumer实例,并创建三个属于group2的Consumer实例,最后通过Producer向topic1发送key分别为1,2,3的消息。结果发现属于group1的Consumer收到了所有的这三条消息,同时group2中的3个Consumer分别收到了key为1,2,3的消息。如下图所示。 73 | 74 | #### 4.1.5 Push vs Pull 75 | 作为一个消息系统,Kafka遵循了传统的方式,选择由Producer向broker push消息并由Consumer从broker pull消息。一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,采用push模式。事实上,push模式和pull模式各有优劣。 76 | push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成Consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据Consumer的消费能力以适当的速率消费消息。 77 | 对于Kafka而言,pull模式更合适。pull模式可简化broker的设计,Consumer可自主控制消费消息的速率,同时Consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。 78 | 79 | #### 4.1.6 Kafka delivery guarantee 80 | 有这么几种可能的delivery guarantee: 81 | 1. At most once 消息可能会丢,但绝不会重复传输 82 | 2. At least one 消息绝不会丢,但可能会重复传输 83 | 3. Exactly once 每条消息肯定会被传输一次且仅传输一次,很多时候这是用户所想要的。 84 |    85 | 当Producer向broker发送消息时,一旦这条消息被commit,因数replica的存在,它就不会丢。但是如果Producer发送数据给broker后,遇到网络问题而造成通信中断,那Producer就无法判断该条消息是否已经commit。虽然Kafka无法确定网络故障期间发生了什么,但是Producer可以生成一种类似于主键的东西,发生故障时幂等性的重试多次,这样就做到了Exactly once。截止到目前(Kafka 0.8.2版本,2015-03-04),这一Feature还并未实现,有希望在Kafka未来的版本中实现。(所以目前默认情况下一条消息从Producer到broker是确保了At least once,可通过设置Producer异步发送实现At most once)。 86 | 87 | 接下来讨论的是消息从broker到Consumer的delivery guarantee语义。(仅针对Kafka consumer high level API)。Consumer在从broker读取消息后,可以选择commit,该操作会在Zookeeper中保存该Consumer在该Partition中读取的消息的offset。该Consumer下一次再读该Partition时会从下一条开始读取。如未commit,下一次读取的开始位置会跟上一次commit之后的开始位置相同。当然可以将Consumer设置为autocommit,即Consumer一旦读到数据立即自动commit。如果只讨论这一读取消息的过程,那Kafka是确保了Exactly once。但实际使用中应用程序并非在Consumer读取完数据就结束了,而是要进行进一步处理,而数据处理与commit的顺序在很大程度上决定了消息从broker和consumer的delivery guarantee semantic。 88 | 读完消息先commit再处理消息。这种模式下,如果Consumer在commit后还没来得及处理消息就crash了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于At most once 89 | 读完消息先处理再commit。这种模式下,如果在处理完消息之后commit之前Consumer crash了,下次重新开始工作时还会处理刚刚未commit的消息,实际上该消息已经被处理过了。这就对应于At least once。在很多使用场景下,消息都有一个主键,所以消息的处理往往具有幂等性,即多次处理这一条消息跟只处理一次是等效的,那就可以认为是Exactly once。(笔者认为这种说法比较牵强,毕竟它不是Kafka本身提供的机制,主键本身也并不能完全保证操作的幂等性。而且实际上我们说delivery guarantee 语义是讨论被处理多少次,而非处理结果怎样,因为处理方式多种多样,我们不应该把处理过程的特性——如是否幂等性,当成Kafka本身的Feature) 90 | 如果一定要做到Exactly once,就需要协调offset和实际操作的输出。经典的做法是引入两阶段提交。如果能让offset和操作输入存在同一个地方,会更简洁和通用。这种方式可能更好,因为许多输出系统可能不支持两阶段提交。比如,Consumer拿到数据后可能把数据放到HDFS,如果把最新的offset和数据本身一起写到HDFS,那就可以保证数据的输出和offset的更新要么都完成,要么都不完成,间接实现Exactly once。(目前就high level API而言,offset是存于Zookeeper中的,无法存于HDFS,而low level API的offset是由自己去维护的,可以将之存于HDFS中) 91 | 92 | 总之,Kafka默认保证At least once,并且允许通过设置Producer异步提交来实现At most once。而Exactly once要求与外部存储系统协作,幸运的是Kafka提供的offset可以非常直接非常容易得使用这种方式。 -------------------------------------------------------------------------------- /4/4.2 Broker详解.md: -------------------------------------------------------------------------------- 1 | ## 4.2 Broker详解 2 | -------------------------------------------------------------------------------- /4/4.3 Producer详解.md: -------------------------------------------------------------------------------- 1 | ## 4.3 Producer详解 2 | 3 | 4 | ```Java 5 | public class ProducerTest { 6 | private static String topicName; 7 | private static int msgNum; 8 | private static int key; 9 | public static void main(String[] args) { 10 | Properties props = new Properties(); 11 | props.put("bootstrap.servers", "127.0.0.1:9092,127.0.0.2:9092"); 12 | props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 13 | props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 14 | topicName = "test"; 15 | String msg = " This is a test message"; 16 | producer.send(new ProducerRecord(topicName, msg)); 17 | ... 18 | producer.close(); 19 | } 20 | } 21 | ``` 22 | 23 | ```Java 24 | public Future send(ProducerRecord record) { 25 | return send(record, null); 26 | } 27 | 28 | public Future send(ProducerRecord record, Callback callback) { 29 | // intercept the record, which can be potentially modified; this method does not throw exceptions 30 | ProducerRecord interceptedRecord = this.interceptors.onSend(record); 31 | return doSend(interceptedRecord, callback); 32 | } 33 | 34 | private Future doSend(ProducerRecord record, Callback callback) { 35 | TopicPartition tp = null; 36 | try { 37 | // first make sure the metadata for the topic is available 38 | ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs); 39 | long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs); 40 | Cluster cluster = clusterAndWaitTime.cluster; 41 | byte[] serializedKey; 42 | //序列化 43 | try { 44 | serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key()); 45 | } catch (ClassCastException cce) { 46 | ... 47 | } 48 | byte[] serializedValue; 49 | try { 50 | serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value()); 51 | } catch (ClassCastException cce) { 52 | ... 53 | } 54 | int partition = partition(record, serializedKey, serializedValue, cluster); 55 | tp = new TopicPartition(record.topic(), partition); 56 | 57 | setReadOnly(record.headers()); 58 | Header[] headers = record.headers().toArray(); 59 | 60 | int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(), 61 | compressionType, serializedKey, serializedValue, headers); 62 | ensureValidRecordSize(serializedSize); 63 | long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp(); 64 | log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition); 65 | // producer callback will make sure to call both 'callback' and interceptor callback 66 | Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp); 67 | 68 | if (transactionManager != null && transactionManager.isTransactional()) 69 | transactionManager.maybeAddPartitionToTransaction(tp); 70 | 71 | RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, 72 | serializedValue, headers, interceptCallback, remainingWaitMs); 73 | if (result.batchIsFull || result.newBatchCreated) { 74 | log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition); 75 | this.sender.wakeup(); 76 | } 77 | return result.future; 78 | // handling exceptions and record the errors; 79 | // for API exceptions return them in the future, 80 | // for other exceptions throw directly 81 | } catch (ApiException e) { 82 | log.debug("Exception occurred during message send:", e); 83 | if (callback != null) 84 | callback.onCompletion(null, e); 85 | this.errors.record(); 86 | this.interceptors.onSendError(record, tp, e); 87 | return new FutureFailure(e); 88 | } catch (InterruptedException e) { 89 | this.errors.record(); 90 | this.interceptors.onSendError(record, tp, e); 91 | throw new InterruptException(e); 92 | } catch (BufferExhaustedException e) { 93 | this.errors.record(); 94 | this.metrics.sensor("buffer-exhausted-records").record(); 95 | this.interceptors.onSendError(record, tp, e); 96 | throw e; 97 | } catch (KafkaException e) { 98 | this.errors.record(); 99 | this.interceptors.onSendError(record, tp, e); 100 | throw e; 101 | } catch (Exception e) { 102 | // we notify interceptor about all exceptions, since onSend is called before anything else in this method 103 | this.interceptors.onSendError(record, tp, e); 104 | throw e; 105 | } 106 | } 107 | ``` -------------------------------------------------------------------------------- /4/4.6 高级消费者-负载均衡.md: -------------------------------------------------------------------------------- 1 | ## 4.6 高级消费者-负载均衡 2 | 3 | ### 4.6.1 简介 4 | Kafka的负载均衡是同一个ConsumerGroup下的不同Consumer线程可以动态消费Topic下的数据。当ConsumerGroup的成员发生变化时(有Consumer加入或者退出时),或者Topic下的Partition发生变化,都要重新分配Partition给存活的Consuemr。 5 | 6 | 前面我们说过Consumer启动时会在/consumers/group/ids/的ZK目录注册自己的id,Kafka集群内部Topic会在/brokers/topics/topic/的ZK目录下注册自己的Partition。Consumer一旦发现以上2个路径的数据发生变化时,就会触发Consumer的负载均衡流程。当Consumer发生负载均衡时,其主要步骤如下所示: 7 | * 关闭所有的ConsumerFetcherThread 8 | * 然后需要删除TopicPartition和ConsumerThread的映射关系,主要包括两部分:ZK上/consumers/group/owners/topic/partition目录下和数据和本地相关数据结构topicRegistry 9 | * 然后重新分配该ConsumerGroup下不同Consumer内部的Consumer线程和Partition的映射关系 10 | * 然后更新TopicPartition和ConsumerThread的映射关系,仍然是要更新ZK上的数据以及本地相关数据 11 | * 最后重新启动所有ConsumerFetcherThread 12 | 13 | 我们在分析reinitializeConsumer方法时,说过Consumer会针对ZK的连接建立、Topic的Partition变化以及Consumer的增删分别建立3个不同Listener,分别是ZKSessionExpireListener、ZKTopicPartitionChangeListener和ZKRebalancerListener,不过最终都是通过ZkRebalancerListener的rebanlace方法完成具体的负载均衡流程的。 14 | 15 | ### 4.6.2 ZKRebalancerListener 16 | 我们直接看rebalance方法: 17 | ```scala 18 | private var topicRegistry = new Pool[String, Pool[Int, PartitionTopicInfo]] 19 | 20 | private def rebalance(cluster: Cluster): Boolean = { 21 | val myTopicThreadIdsMap = TopicCount.constructTopicCount( 22 | group, consumerIdString, zkUtils, config.excludeInternalTopics).getConsumerThreadIdsPerTopic 23 | //获取当前Kafka集群中在线的Broker列表 24 | val brokers = zkUtils.getAllBrokersInCluster() 25 | if (brokers.size == 0) { 26 | zkUtils.subscribeChildChanges(BrokerIdsPath, loadBalancerListener) 27 | true 28 | } else { 29 | //关闭ConsumerFetcherThread线程 30 | closeFetchers(cluster, kafkaMessageAndMetadataStreams, myTopicThreadIdsMap) 31 | if (consumerRebalanceListener != null) { 32 | consumerRebalanceListener.beforeReleasingPartitions( 33 | if (topicRegistry.size == 0) 34 | new java.util.HashMap[String, java.util.Set[java.lang.Integer]] 35 | else 36 | topicRegistry.map(topics => 37 | topics._1 -> topics._2.keys 38 | ).toMap.asJava.asInstanceOf[java.util.Map[String, java.util.Set[java.lang.Integer]]] 39 | ) 40 | } 41 | //删除TopicPartition和ConsumerThread的映射关系 42 | releasePartitionOwnership(topicRegistry) 43 | //重新分配ConsumerThread和Partition 44 | val assignmentContext = new AssignmentContext(group, consumerIdString, config.excludeInternalTopics, zkUtils) 45 | val globalPartitionAssignment = partitionAssignor.assign(assignmentContext) 46 | val partitionAssignment = globalPartitionAssignment.get(assignmentContext.consumerId) 47 | val currentTopicRegistry = new Pool[String, Pool[Int, PartitionTopicInfo]]( 48 | valueFactory = Some((_: String) => new Pool[Int, PartitionTopicInfo])) 49 | 50 | //获取当前topicPartitions的offset 51 | val topicPartitions = partitionAssignment.keySet.toSeq 52 | val offsetFetchResponseOpt = fetchOffsets(topicPartitions) 53 | 54 | if (isShuttingDown.get || !offsetFetchResponseOpt.isDefined) 55 | false 56 | else { 57 | val offsetFetchResponse = offsetFetchResponseOpt.get 58 | //更新currentTopicRegistry,主要是重新创建TopicPartition对应的PartitionTopicInfo信息 59 | topicPartitions.foreach { case tp@TopicAndPartition(topic, partition) => 60 | val offset = offsetFetchResponse.requestInfo(tp).offset 61 | val threadId = partitionAssignment(tp) 62 | addPartitionTopicInfo(currentTopicRegistry, partition, topic, offset, threadId) 63 | } 64 | 65 | //更新/consumers/group/owners/topic/partition路径下的映射关系 66 | if (reflectPartitionOwnershipDecision(partitionAssignment)) { 67 | allTopicsOwnedPartitionsCount = partitionAssignment.size 68 | topicRegistry = currentTopicRegistry 69 | 70 | //重新启动ConsumerFetcherThread线程 71 | updateFetcher(cluster) 72 | true 73 | } else { 74 | false 75 | } 76 | } 77 | } 78 | } 79 | ``` 80 | 这里简单分析一下该方法: 81 | * 首先关闭所有的ConsumerFetcherThread 82 | * 然后删除TopicPartition和ConsumerThread的映射关系:删除ZK上/consumers/group/owners/topic/partition路径下的数据并清理本地数据结构topicRegistry(TopicPartition和ConsumerThread的映射关系) 83 | * 然后重新分配该ConsumerGroup下TopicPartition和ConsumerThread 84 | * 然后更新本地数据结构topicRegistry,并更新ZK上/consumers/group/owners/topic/partition下的数据 85 | * 最后重新启动所有ConsumerFetcherThread 86 | 87 | 接下来,我们会详细分析一下这几个步骤。 88 | 89 | #### closeFetchers 90 | 我们首先看一下closeFetchers方法,该方法逻辑比较简单:主要是清理Queue,并shutdown所有的ConsumerFetcherThread。 91 | ```scala 92 | private def closeFetchers(cluster: Cluster, messageStreams: Map[String, List[KafkaStream[_, _]]], 93 | relevantTopicThreadIdsMap: Map[String, Set[ConsumerThreadId]]) { 94 | val queuesTobeCleared = topicThreadIdAndQueues.filter(q => relevantTopicThreadIdsMap.contains(q._1._1)).map(q => q._2) 95 | closeFetchersForQueues(cluster, messageStreams, queuesTobeCleared) 96 | } 97 | private def closeFetchersForQueues(cluster: Cluster, 98 | messageStreams: Map[String, List[KafkaStream[_, _]]], 99 | queuesToBeCleared: Iterable[BlockingQueue[FetchedDataChunk]]) { 100 | val allPartitionInfos = topicRegistry.values.map(p => p.values).flatten 101 | fetcher.foreach { f => 102 | f.stopConnections() 103 | clearFetcherQueues(allPartitionInfos, cluster, queuesToBeCleared, messageStreams) 104 | if (config.autoCommitEnable) { 105 | commitOffsets(true) 106 | } 107 | } 108 | } 109 | def stopConnections() { 110 | if (leaderFinderThread != null) { 111 | leaderFinderThread.shutdown() 112 | leaderFinderThread = null 113 | } 114 | 115 | closeAllFetchers() 116 | 117 | partitionMap = null 118 | noLeaderPartitionSet.clear() 119 | } 120 | private def clearFetcherQueues(topicInfos: Iterable[PartitionTopicInfo], cluster: Cluster, 121 | queuesTobeCleared: Iterable[BlockingQueue[FetchedDataChunk]], 122 | messageStreams: Map[String, List[KafkaStream[_, _]]]) { 123 | 124 | // Clear all but the currently iterated upon chunk in the consumer thread's queue 125 | queuesTobeCleared.foreach(_.clear) 126 | info("Cleared all relevant queues for this fetcher") 127 | 128 | // Also clear the currently iterated upon chunk in the consumer threads 129 | if (messageStreams != null) 130 | messageStreams.foreach(_._2.foreach(s => s.clear())) 131 | } 132 | def closeAllFetchers() { 133 | lock synchronized { 134 | for ((_, fetcher) <- fetcherThreadMap) { 135 | fetcher.initiateShutdown() 136 | } 137 | 138 | for ((_, fetcher) <- fetcherThreadMap) { 139 | fetcher.shutdown() 140 | } 141 | fetcherThreadMap.clear() 142 | } 143 | } 144 | ``` 145 | 146 | #### 清理TopicPartition和ConsumerThread的映射关系 147 | 关闭了ConsumerFetcherThread后,就直接调用releasePartitionOwnership方法清理TopicPartition和ConsumerThread的映射关系: 148 | ```scala 149 | private def releasePartitionOwnership(localTopicRegistry: Pool[String, Pool[Int, PartitionTopicInfo]]) = { 150 | for ((topic, infos) <- localTopicRegistry) { 151 | for (partition <- infos.keys) { 152 | deletePartitionOwnershipFromZK(topic, partition) 153 | } 154 | localTopicRegistry.remove(topic) 155 | } 156 | allTopicsOwnedPartitionsCount = 0 157 | } 158 | ``` 159 | 该方法比较简单:就是遍历topicRegistry,从ZK上删除映射关系,并清空topicRegistry。 160 | 161 | #### 重新分配TopicPartition和ConsumerThread 162 | 首先我们看一下AssignmentContext,它记录当前ConsumerGroup下的分配上下文信息,主要是每个Topic的订阅ConsumerThreads和Partitions。具体代码如下所示: 163 | ```scala 164 | class AssignmentContext(group: String, val consumerId: String, excludeInternalTopics: Boolean, zkUtils: ZkUtils) { 165 | //当前Consumer消费的Topic和Thread 166 | val myTopicThreadIds: collection.Map[String, collection.Set[ConsumerThreadId]] = { 167 | val myTopicCount = TopicCount.constructTopicCount(group, consumerId, zkUtils, excludeInternalTopics) 168 | myTopicCount.getConsumerThreadIdsPerTopic 169 | } 170 | 171 | //map of topic -> consumers:该Group下Topic的订阅关系,从ZK目录/consumers/group/ids/上拉取 172 | val consumersForTopic: collection.Map[String, List[ConsumerThreadId]] = 173 | zkUtils.getConsumersPerTopic(group, excludeInternalTopics) 174 | 175 | //map of topic -> partitons:该Topic下的所有Partitions 176 | val partitionsForTopic: collection.Map[String, Seq[Int]] = 177 | zkUtils.getPartitionsForTopics(consumersForTopic.keySet.toSeq) 178 | 179 | val consumers: Seq[String] = zkUtils.getConsumersInGroup(group).sorted 180 | } 181 | ``` 182 | 其中consumersForTopic:以Topic为key,记录所有订阅该Topic的ConsumerThreads;partitionsForTopic:以Topic为key,记录该Topic下的所有Partitions。 183 | 184 | 接下来我们以RangeAssignor为例简单说明一下如何分配Partitions给ConsumerThreads: 185 | ```scala 186 | class RangeAssignor() extends PartitionAssignor with Logging { 187 | 188 | def assign(ctx: AssignmentContext) = { 189 | val valueFactory = (_: String) => new mutable.HashMap[TopicAndPartition, ConsumerThreadId] 190 | val partitionAssignment = 191 | new Pool[String, mutable.Map[TopicAndPartition, ConsumerThreadId]](Some(valueFactory)) 192 | //遍历当前Consumer订阅的每个Topic的Consumer线程 193 | for (topic <- ctx.myTopicThreadIds.keySet) { 194 | //取出当前ConsumerGroup下订阅这个Topic的所有Consumers 195 | val curConsumers = ctx.consumersForTopic(topic) 196 | //取出这个Topic的所有Partitions 197 | val curPartitions: Seq[Int] = ctx.partitionsForTopic(topic) 198 | 199 | //计算平均每个Consumer线程消费几个Partition 200 | val nPartsPerConsumer = curPartitions.size / curConsumers.size 201 | val nConsumersWithExtraPart = curPartitions.size % curConsumers.size 202 | 203 | //遍历该Topic下的所有Consumer线程:将该Topic下的所有Partition分配给这些线程 204 | for (consumerThreadId <- curConsumers) { 205 | val myConsumerPosition = curConsumers.indexOf(consumerThreadId) 206 | assert(myConsumerPosition >= 0) 207 | val startPart = nPartsPerConsumer * myConsumerPosition + myConsumerPosition.min(nConsumersWithExtraPart) 208 | val nParts = nPartsPerConsumer + (if (myConsumerPosition + 1 > nConsumersWithExtraPart) 0 else 1) 209 | 210 | if (nParts <= 0) 211 | warn("No broker partitions consumed by consumer thread " + consumerThreadId + " for topic " + topic) 212 | else { 213 | //记录TopicPartition和Consumer线程的映射关系 214 | for (i <- startPart until startPart + nParts) { 215 | val partition = curPartitions(i) 216 | val assignmentForConsumer = partitionAssignment.getAndMaybePut(consumerThreadId.consumer) 217 | assignmentForConsumer += (TopicAndPartition(topic, partition) -> consumerThreadId) 218 | } 219 | } 220 | } 221 | } 222 | 223 | // assign Map.empty for the consumers which are not associated with topic partitions 224 | ctx.consumers.foreach(consumerId => partitionAssignment.getAndMaybePut(consumerId)) 225 | partitionAssignment 226 | } 227 | } 228 | ``` 229 | 230 | #### 更新topicRegistry 231 | 重新分配完TopicPartition和ConsumerThread之间的映射关系后,需要更新相关数据,首先需要更新TopicParition数据(offset),然后调用addPartitionTopicInfo重新创建PartitionTopicInfo并添加到本地topicRegistry中,然后调用reflectPartitionOwnershipDecision方法更新ZK上的相关数据。 232 | ```scala 233 | private def addPartitionTopicInfo(currentTopicRegistry: Pool[String, Pool[Int, PartitionTopicInfo]], 234 | partition: Int, topic: String, 235 | offset: Long, consumerThreadId: ConsumerThreadId) { 236 | val partTopicInfoMap = currentTopicRegistry.getAndMaybePut(topic) 237 | 238 | val queue = topicThreadIdAndQueues.get((topic, consumerThreadId)) 239 | val consumedOffset = new AtomicLong(offset) 240 | val fetchedOffset = new AtomicLong(offset) 241 | val partTopicInfo = new PartitionTopicInfo(topic, 242 | partition, 243 | queue, 244 | consumedOffset, 245 | fetchedOffset, 246 | new AtomicInteger(config.fetchMessageMaxBytes), 247 | config.clientId) 248 | partTopicInfoMap.put(partition, partTopicInfo) 249 | checkpointedZkOffsets.put(TopicAndPartition(topic, partition), offset) 250 | } 251 | 252 | private def reflectPartitionOwnershipDecision(partitionAssignment: Map[TopicAndPartition, ConsumerThreadId]): Boolean = { 253 | var successfullyOwnedPartitions: List[(String, Int)] = Nil 254 | val partitionOwnershipSuccessful = partitionAssignment.map { partitionOwner => 255 | val topic = partitionOwner._1.topic 256 | val partition = partitionOwner._1.partition 257 | val consumerThreadId = partitionOwner._2 258 | val partitionOwnerPath = zkUtils.getConsumerPartitionOwnerPath(group, topic, partition) 259 | try { 260 | zkUtils.createEphemeralPathExpectConflict(partitionOwnerPath, consumerThreadId.toString) 261 | successfullyOwnedPartitions ::= (topic, partition) 262 | true 263 | } catch { 264 | case _: ZkNodeExistsException => 265 | // The node hasn't been deleted by the original owner. So wait a bit and retry. 266 | false 267 | } 268 | } 269 | val hasPartitionOwnershipFailed = partitionOwnershipSuccessful.foldLeft(0)((sum, decision) => sum + (if (decision) 0 else 1)) 270 | if (hasPartitionOwnershipFailed > 0) { 271 | // remove all paths that we have owned in ZK 272 | successfullyOwnedPartitions.foreach(topicAndPartition => deletePartitionOwnershipFromZK(topicAndPartition._1, topicAndPartition._2)) 273 | false 274 | } 275 | else true 276 | } 277 | ``` 278 | 279 | #### 重新启动ConsumerFetcherThread 280 | 最后重新启动ConsumerFetcherThread,开始拉取数据。 281 | ```scala 282 | private def updateFetcher(cluster: Cluster) { 283 | // update partitions for fetcher 284 | var allPartitionInfos: List[PartitionTopicInfo] = Nil 285 | for (partitionInfos <- topicRegistry.values) 286 | for (partition <- partitionInfos.values) 287 | allPartitionInfos ::= partition 288 | 289 | fetcher.foreach(_.startConnections(allPartitionInfos, cluster)) 290 | } 291 | ``` 292 | -------------------------------------------------------------------------------- /4/4.7 高级消费者-offset管理.md: -------------------------------------------------------------------------------- 1 | ## 4.7 高级消费者-offset管理 2 | 3 | 我们知道,Consumer抓取数据之前要先获取到该Partition的Offset,这样Fetcher才能知道要从Partition的哪个fetchOffset开始抓取数据。同时Consumer会定时地commitOffset。根据Offset信息保存在Zk还是Kafka上分为两种情况,这里以Kafka为例,简单分析一下该流程: 4 | * 首先初始化GroupCoordinator的通信连接offsetsChannel,用于之后的fetchOffset和commitOffset 5 | * Consumer抓取数据时首先通过offsetsChannel,发送OffsetFetchRequest,以获取offset 6 | * Consumer会不停的commitOffset,即通过offsetsChannel,发送OffsetCommitRequest请求,保存offset 7 | 8 | ### 4.7.1 OffsetManager 9 | 前面说过,在创建ZookeeperConsumerConnector时会调用ensureOffsetManagerConnected方法建立到GroupCoordinator上的OffsetManager的通道,即OffsetChannel。 10 | ```scala 11 | // Blocks until the offset manager is located and a channel is established to it. 12 | private def ensureOffsetManagerConnected() { 13 | if (config.offsetsStorage == "kafka") { 14 | if (offsetsChannel == null || !offsetsChannel.isConnected) 15 | offsetsChannel = ClientUtils.channelToOffsetManager(config.groupId, zkUtils, ..) 16 | } 17 | } 18 | ``` 19 | ```scala 20 | def channelToOffsetManager(group: String, zkUtils: ZkUtils, socketTimeoutMs: Int = 3000, retryBackOffMs: Int = 1000) = { 21 | //从/brokers/ids中寻找一个在线的Broker,然后和其建立连接 22 | var queryChannel = channelToAnyBroker(zkUtils) 23 | 24 | var offsetManagerChannelOpt: Option[BlockingChannel] = None 25 | //直到和coordinator建立通信连接为止 26 | while (offsetManagerChannelOpt.isEmpty) { 27 | var coordinatorOpt: Option[BrokerEndPoint] = None 28 | while (coordinatorOpt.isEmpty) { 29 | try { 30 | //通信连接断开,则重新连接 31 | if (!queryChannel.isConnected) 32 | queryChannel = channelToAnyBroker(zkUtils) 33 | //发送GroupCoordinatorRequest 34 | queryChannel.send(GroupCoordinatorRequest(group)) 35 | val response = queryChannel.receive() 36 | //接收ConsumerMatadataResponse 37 | val consumerMetadataResponse = GroupCoordinatorResponse.readFrom(response.payload()) 38 | if (consumerMetadataResponse.error == Errors.NONE) 39 | coordinatorOpt = consumerMetadataResponse.coordinatorOpt 40 | else { 41 | Thread.sleep(retryBackOffMs) 42 | } 43 | } catch { 44 | ... 45 | } 46 | } 47 | 48 | val coordinator = coordinatorOpt.get 49 | if (coordinator.host == queryChannel.host && coordinator.port == queryChannel.port) { 50 | //如果coordinator和queryChannel相同,则直接返回queryChannel 51 | offsetManagerChannelOpt = Some(queryChannel) 52 | } else { 53 | //不同,则连接到目标coordinator,并断开当前queryChannel 54 | var offsetManagerChannel: BlockingChannel = null 55 | try { 56 | offsetManagerChannel = new BlockingChannel(coordinator.host, coordinator.port, 57 | BlockingChannel.UseDefaultBufferSize, 58 | BlockingChannel.UseDefaultBufferSize, 59 | socketTimeoutMs) 60 | offsetManagerChannel.connect() 61 | offsetManagerChannelOpt = Some(offsetManagerChannel) 62 | queryChannel.disconnect() 63 | } catch { 64 | ... 65 | } 66 | } 67 | } 68 | 69 | offsetManagerChannelOpt.get 70 | } 71 | ``` 72 | 简单分析一下该方法: 73 | * 首先调用channelToAnyBroker方法,任意选取一个Broker和其建立连接queryChannel 74 | * 然后通过queryChannel发送GroupCoordinatorRequest,并接收ConsumerMetadataResponse 75 | * 解析该Response,获取coordinator的host和port信息,如果和当前queryChannel信息一致,说明GroupCoordinator就是该Broker,则直接返回该queryChannel;否则建立到该GroupCoordinator的连接,并断开当前queryChannel。 76 | 77 | ### 4.7.2 fetchOffset 78 | 前面我们说过,Consumer开始消费数据之前,必须首先fetchOffsets。这里分为从Zk或者Kafka上获取两种情况:Zk上则是读取/consumers/group/offsets/topic/partition目录下的数据,Kafka上则是通过offsetsChannel发送OffsetFetchRequest来获取,具体逻辑如下所示: 79 | ```scala 80 | private def fetchOffsets(partitions: Seq[TopicAndPartition]) = { 81 | if (partitions.isEmpty) 82 | Some(OffsetFetchResponse(Map.empty)) 83 | else if (config.offsetsStorage == "zookeeper") { 84 | //从zk上获取 85 | val offsets = partitions.map(fetchOffsetFromZooKeeper) 86 | Some(OffsetFetchResponse(immutable.Map(offsets: _*))) 87 | } else { 88 | //构造OffsetFetchRequest 89 | val offsetFetchRequest = OffsetFetchRequest(groupId = config.groupId, requestInfo = partitions, clientId = config.clientId) 90 | 91 | var offsetFetchResponseOpt: Option[OffsetFetchResponse] = None 92 | while (!isShuttingDown.get && !offsetFetchResponseOpt.isDefined) { 93 | offsetFetchResponseOpt = offsetsChannelLock synchronized { 94 | ensureOffsetManagerConnected() 95 | try { 96 | //发送OffsetFetchRequest 97 | offsetsChannel.send(offsetFetchRequest) 98 | //读取OffsetFetchResponse 99 | val offsetFetchResponse = OffsetFetchResponse.readFrom(offsetsChannel.receive().payload()) 100 | 101 | val (leaderChanged, loadInProgress) = 102 | offsetFetchResponse.requestInfo.values.foldLeft(false, false) { case (folded, offsetMetadataAndError) => 103 | (folded._1 || (offsetMetadataAndError.error == Errors.NOT_COORDINATOR), 104 | folded._2 || (offsetMetadataAndError.error == Errors.COORDINATOR_LOAD_IN_PROGRESS)) 105 | } 106 | 107 | if (leaderChanged) { 108 | offsetsChannel.disconnect() 109 | None // retry 110 | } 111 | else if (loadInProgress) { 112 | debug("Could not fetch offsets (because offset cache is being loaded).") 113 | None // retry 114 | } 115 | else { 116 | if (config.dualCommitEnabled) { 117 | // if dual-commit is enabled, then pick the max between offsets in zookeeper and kafka. 118 | val kafkaOffsets = offsetFetchResponse.requestInfo 119 | val mostRecentOffsets = kafkaOffsets.map { case (topicPartition, kafkaOffset) => 120 | val zkOffset = fetchOffsetFromZooKeeper(topicPartition)._2.offset 121 | val mostRecentOffset = zkOffset.max(kafkaOffset.offset) 122 | (topicPartition, OffsetMetadataAndError(mostRecentOffset, kafkaOffset.metadata, Errors.NONE)) 123 | } 124 | Some(OffsetFetchResponse(mostRecentOffsets)) 125 | } 126 | else 127 | Some(offsetFetchResponse) 128 | } 129 | } catch { 130 | ... 131 | } 132 | } 133 | 134 | if (offsetFetchResponseOpt.isEmpty) { 135 | Thread.sleep(config.offsetsChannelBackoffMs) 136 | } 137 | } 138 | 139 | offsetFetchResponseOpt 140 | } 141 | } 142 | ``` 143 | 144 | ### 4.7.3 commitOffset 145 | commitOffset时,会将当前Comsumer消费的offset提交到Zk或者Kafka中,提交到Zk时会更新/consumers/group/offsets/topic/partition目录下的数据,提交到Kafka则是利用offsetsChannel发送OffsetCommitRequest,具体实现如下所示: 146 | ```scala 147 | def commitOffsets(isAutoCommit: Boolean) { 148 | val offsetsToCommit = 149 | immutable.Map(topicRegistry.values.flatMap { partitionTopicInfos => 150 | partitionTopicInfos.values.map { info => 151 | TopicAndPartition(info.topic, info.partitionId) -> OffsetAndMetadata(info.getConsumeOffset()) 152 | } 153 | }.toSeq: _*) 154 | 155 | commitOffsets(offsetsToCommit, isAutoCommit) 156 | } 157 | def commitOffsets(offsetsToCommit: immutable.Map[TopicAndPartition, OffsetAndMetadata], isAutoCommit: Boolean) { 158 | //计算重试次数,由offsets.commit.max.retries决定,默认是5 159 | var retriesRemaining = 1 + (if (isAutoCommit) 0 else config.offsetsCommitMaxRetries) 160 | var done = false 161 | while (!done) { 162 | val committed = offsetsChannelLock synchronized { 163 | // committed when we receive either no error codes or only MetadataTooLarge errors 164 | //提取需要提交的offset(topic、partition和offset) 165 | if (offsetsToCommit.size > 0) { 166 | if (config.offsetsStorage == "zookeeper") { 167 | //提交Zookeeper上 168 | offsetsToCommit.foreach { case (topicAndPartition, offsetAndMetadata) => 169 | commitOffsetToZooKeeper(topicAndPartition, offsetAndMetadata.offset) 170 | } 171 | true 172 | } else { 173 | //提交到Kafka,组装OffsetCommitRequest 174 | val offsetCommitRequest = OffsetCommitRequest(config.groupId, offsetsToCommit, clientId = config.clientId) 175 | ensureOffsetManagerConnected() 176 | try { 177 | kafkaCommitMeter.mark(offsetsToCommit.size) 178 | //发送OffsetCommitRequest,并读取Response 179 | offsetsChannel.send(offsetCommitRequest) 180 | val offsetCommitResponse = OffsetCommitResponse.readFrom(offsetsChannel.receive().payload()) 181 | 182 | val (commitFailed, retryableIfFailed, shouldRefreshCoordinator, errorCount) = { 183 | offsetCommitResponse.commitStatus.foldLeft(false, false, false, 0) { case (folded, (topicPartition, error)) => 184 | 185 | if (error == Errors.NONE && config.dualCommitEnabled) { 186 | val offset = offsetsToCommit(topicPartition).offset 187 | commitOffsetToZooKeeper(topicPartition, offset) 188 | } 189 | 190 | (folded._1 || // update commitFailed 191 | error != Errors.NONE, 192 | 193 | folded._2 || // update retryableIfFailed - (only metadata too large is not retryable) 194 | (error != Errors.NONE && error != Errors.OFFSET_METADATA_TOO_LARGE), 195 | 196 | folded._3 || // update shouldRefreshCoordinator 197 | error == Errors.NOT_COORDINATOR || 198 | error == Errors.COORDINATOR_NOT_AVAILABLE, 199 | 200 | // update error count 201 | folded._4 + (if (error != Errors.NONE) 1 else 0)) 202 | } 203 | } 204 | 205 | if (shouldRefreshCoordinator) { 206 | offsetsChannel.disconnect() 207 | } 208 | 209 | if (commitFailed && retryableIfFailed) 210 | false 211 | else 212 | true 213 | } catch { 214 | ... 215 | } 216 | } 217 | } else { 218 | true 219 | } 220 | } 221 | 222 | //判断是否完成 223 | done = { 224 | retriesRemaining -= 1 225 | retriesRemaining == 0 || committed 226 | } 227 | 228 | //未完成,则休眠offsets.channel.backoff.ms毫秒 229 | if (!done) { 230 | Thread.sleep(config.offsetsChannelBackoffMs) 231 | } 232 | } 233 | } 234 | ``` 235 | -------------------------------------------------------------------------------- /4/4.8 GroupCoordinator.md: -------------------------------------------------------------------------------- 1 | ## 4.8 GroupCoordinator 2 | 3 | 在kafka0.9.0版本的时候,增加了GroupCoordinator这个角色,每个KafkaServer都有一个GroupCoordinator实例,GroupCoordinator 是负责进行 consumer 的 group 成员与 offset 管理 4 | 5 | 需要注意的是GroupCoordinator集群的方式与 6 | 每个Broker上都会运行一个GroupCoordinator实例,Kafka按照ConsumerGroup的名称将其分配给对应的GroupCoordinator进行管理 7 | 8 | ### rebalance时机 9 | 在如下条件下,partition要在consumer中重新分配: 10 | * consumer数量变化:有新的consumer加入以及退出等 11 | * topic的partition数量发生了变化 12 | * coordinator挂了,集群选举出新的coordinator 13 | 14 | ### __consumer_offsets 15 | 由于Zk的写性能不佳,这个版本将 topic 的 offset 信息由之前存储在 zookeeper(/consumers//offsets//) 上改为存储到一个特殊的 topic 中(__consumer_offsets)。 16 | 17 | Consumer通过发送OffsetCommitRequest请求到指定broker(OffsetManager)提交偏移量。OffsetManager会以追加键值(key-value)形式的消息到这个指定的topic(__consumer_offsets)。key是由consumerGroup-topic-partition组成的,而value是偏移量。 18 | 19 | 内存中也会维护一份最近的记录,为了在指定key的情况下能快速的给出OffsetFetchRequests而不用扫描全部偏移量topic日志。如果OffsetManager因某种原因失败,新的broker将会成为偏移量管理者并且通过扫描偏移量topic来重新生成偏移量缓存。 -------------------------------------------------------------------------------- /5/5.1 类型系统.md: -------------------------------------------------------------------------------- 1 | ## 5.1 类型系统 2 | 3 | ### 5.1.1 slice 4 | slice表示一个拥有相同类型元素的可变长度的序列。简单看一个示例 5 | ```go 6 | 7 | ``` 8 | 9 | slice是对数组一个连续片段的引用,即其是一个引用类型(因此更类似于C/C++中的数组)。这个片段可以是整个数组,也可以是该数组的一个子集(起始地址和len)。其内部实现的数据结构通过指针引用底层数组,设定相关属性(len)将数据读写操作限定在指定的区域内。具体如下所是: 10 | ```go 11 | type slice struct { 12 | array unsafe.Pointer 13 | len int 14 | cap int 15 | } 16 | ``` 17 | 18 | #### slice注意事项 19 | 这里简单总结一下slice使用时的注意事项: 20 | ```go 21 | func printNums() { 22 | nums := make([]int, 0, 8) 23 | //wrong 24 | nums := make([]int, 8) 25 | for i := 0; i < 8; i++ { 26 | nums = append(nums, i) 27 | } 28 | 29 | fmt.Println(nums) // [0 1 2 3 4 5 6 7] 30 | } 31 | ``` 32 | make创建slice,只应该使用以下两种方式: 33 | ```go 34 | //已知长度时 35 | nums := make([]int, 0, 8) 36 | //不确定长度时 37 | nums := make([]int, 0) 38 | ``` 39 | 40 | ```go 41 | func modifySlice() { 42 | path := []byte("user/stephen") 43 | sepIndex := bytes.IndexByte(path, '/') 44 | dir1 := make([]byte, sepIndex) 45 | //wrong 46 | dir1 := path[:sepIndex] 47 | copy(dir1, path[:sepIndex]) 48 | 49 | dir2 := path[sepIndex+1:] 50 | fmt.Println("dir1", string(dir1)) 51 | fmt.Println("dir2", string(dir2)) 52 | 53 | dir1 = append(dir1, "test"...) 54 | fmt.Println("dir1", string(dir1)) 55 | fmt.Println("dir2", string(dir2)) 56 | } 57 | ``` 58 | 这里简单说明一下slice的使用规则: 59 | * truncation操作后,指向的是同一个array,如果需要修改,则使用copy方法 60 | * append操作后,指向的可能不是同一个array了 61 | 62 | ### 5.1.2 func 63 | 这里简单讲解一下defer关键字。defer用于资源的释放,会在函数返回之前进行调用。一般采用如下模式: 64 | ```go 65 | f,err := os.Open(filename) 66 | if err != nil { 67 | panic(err) 68 | } 69 | defer f.Close() 70 | ``` 71 | 如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。 72 | 73 | 不过如果对defer的了解不够深入,使用起来可能会踩到一些坑,尤其是跟带命名的返回参数一起使用时。在讲解defer的实现之前先看一看使用defer容易遇到的问题。 74 | 75 | #### defer使用时的坑 76 | 先来看看几个例子: 77 | ```go 78 | func f1() (result int) { 79 | defer func() { 80 | result++ 81 | }() 82 | return 0 83 | } 84 | 85 | func f2() (r int) { 86 | t := 5 87 | defer func() { 88 | t += 5 89 | }() 90 | return t 91 | } 92 | 93 | func f3() (r int) { 94 | defer func(r int) { 95 | r = r + 5 96 | }(r) 97 | return 1 98 | } 99 | ``` 100 | 这里f1返回1,f2返回5,f3返回1... 101 | 102 | 前面我们说过defer是在return之前执行的,但是需要注意的是return语句并不是一条原子指令。函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。 103 | 104 | defer和return结合,实际上执行流程是如下所是: 105 | ``` 106 | 返回值 = xxx 107 | 调用defer函数 108 | 空的return 109 | ``` 110 | 我们在看一下之前的例子: 111 | ```go 112 | func f1() (result int) { 113 | result = 0 //返回值赋值 114 | func() { //defer语句 115 | result++ 116 | }() 117 | return //return指令 118 | } 119 | 120 | func f2() (r int) { 121 | t := 5 122 | r = t //返回值赋值 123 | func() { //defer语句 124 | t = t + 5 125 | } 126 | return //return指令 127 | } 128 | 129 | func f3() (r int) { 130 | r = 1 //返回值赋值 131 | func(r int) { //defer语句:go默认是值传递 132 | r = r + 5 133 | }(r) 134 | return //return指令 135 | } 136 | 所以这个例子的结果是1。 137 | ``` 138 | 139 | -------------------------------------------------------------------------------- /5/5.2 指针与可寻址性.md: -------------------------------------------------------------------------------- 1 | ## 5.2 指针和可寻址性 2 | 3 | ### 可寻址性 4 | go规范定义,只有当变量x是以下几种方式时,它才是可寻址的: 5 | * 一个变量: &x 6 | * 指针引用(pointer indirection): &*x 7 | * slice索引操作(不管slice是否可寻址,slice底层是一个数组指针): &s[1] 8 | * 可寻址数组的索引操作: &a[0] 9 | * 可寻址struct的字段: &point.X 10 | * composite literal类型: &struct{ X int }{1} 11 | 12 | 而以下几种情况是不可以寻址的: 13 | * 常数 14 | * 字符串中的字节 15 | * map对象中的元素 16 | * 接口对象的动态值(通过type assertions获得) 17 | * literal值(非composite literal) 18 | * package级别的函数 19 | * 方法method (用作函数值) 20 | * 中间值(intermediate value):函数调用、显式类型转换、各种类型的操作(除了指针引用pointer dereference操作 *x) 21 | 22 | 这里有几个地方需要解释一下: 23 | * 常数为不能寻址:如果可以寻址的话,我们可以通过指针修改常数的值,破坏了常数的定义。 24 | * 字符串中的字符/字节不能寻址:同上,因为字符串是不可变的。 25 | * map的元素不能寻址:两个原因,如果对象不存在,则返回零值,零值是不可变对象,所以不能寻址;如果对象存在,Go中map中元素的地址是变化的,这意味着寻址的结果是无意义的。 26 | * slice中的元素总是可以寻址:因为slice底层是一个数组指针,它是可以寻址的。 27 | 28 | ### reflect.Value的CanAddr 29 | 在我们使用reflect执行一些底层的操作的时候,经常会使用到reflect.Value的CanSet方法来判断该字段是否可以赋值。CanSet比CanAddr只加了一个限制,就是struct的unexported的字段不能Set,所以这里我们主要介绍一下CanAddr。 30 | 31 | 只有下面的类型reflect.Value的CanAddr才是true(即可以addressable): 32 | * slice的元素 33 | * 可寻址数组的元素 34 | * 可寻址struct的字段 35 | * 指针引用的结果 36 | 37 | 与规范中规定的addressable,reflect.Value的addressable范围有所缩小。比如对于栈上分配的变量,随着方法的生命周期的结束,栈上的对象也就被回收掉了,这个时候如果获取它们的地址,就会出现不一致的结果,甚至安全问题。所以如果你想通过reflect.Value对它的值进行更新,应该先调用CanSet方法判断其是否可以赋值。 38 | 39 | 40 | -------------------------------------------------------------------------------- /5/5.3 协程和通道.md: -------------------------------------------------------------------------------- 1 | ## 5.3 协程与通道 2 | 3 | ### 协程 4 | 5 | ### 通道 6 | 7 | -------------------------------------------------------------------------------- /5/5.4 接口和反射.md: -------------------------------------------------------------------------------- 1 | ## 5.3 接口与反射 2 | 3 | ### 接口 4 | 一个接口变量存储了一对(value, type):赋值给这个接口变量的具体值value、以及这个值的类型描述符type; 5 | 6 | ### 反射 7 | 我们知道,Go中的interface变量也是有静态类型(声明时指定的接口类型),和实际类型(实际存储的数据类型);而反射是一种检查存储在接口变量中的type&value对的机制,反射操作所需的全部信息都源自接口变量(通过把变量转换为空接口变量,从而获得了该变量的type&value信息,从而进行反射操作)。 8 | 9 | reflect包中的两个类型:Type和Value,这两种类型提供了访问一个接口变量中所包含的type&value信息的途径。 10 | 11 | #### Type 12 | Type接口:可以表示一个Go类型,我们可以使用TypeOf方法获取一个变量的Type信息 13 | ``` 14 | func TypeOf(i interface{}) Type 15 | ``` 16 | reflect.Typeof(x),形参x被保存为一个接口值并作为参数传递(复制),方法内部会把该接口值拆包恢复出x的类型信息保存为reflect.Type并返回。 17 | 18 | 接下来我们简单看一下Type接口: 19 | ``` 20 | type Type interface { 21 | // Method returns the i'th method in the type's method set. 22 | Method(int) Method 23 | // MethodByName returns the method with that name in the type's method set and a boolean indicating if the method was found. 24 | MethodByName(string) (Method, bool) 25 | 26 | // Kind returns the specific kind of this type. 27 | Kind() Kind 28 | 29 | // Comparable reports whether values of this type are comparable. 30 | Comparable() bool 31 | 32 | // Elem returns a type's element type. It panics if the type's Kind is not Array, Chan, Map, Ptr, or Slice. 33 | Elem() Type 34 | 35 | // Field returns a struct type's i'th field. It panics if the type's Kind is not Struct. 36 | Field(i int) StructField 37 | // FieldByName returns the struct field with the given name and a boolean indicating if the field was found. 38 | FieldByName(name string) (StructField, bool) 39 | } 40 | ``` 41 | 简单说明一下这些方法: 42 | * Field()系列方法将返回对应的成员 43 | * Method()系列方法将返回对应的方法 44 | * Kind()将返回一个常量,表示具体类型的底层类型 45 | * Elem()方法返回pointer、array、slice、map和chan的基类型 46 | 47 | 可用反射提取struct tag,还能自动分解,常用于ORM映射、数据验证等; 48 | 49 | #### Value 50 | Value接口:可以表示一个Go值,我们可以使用ValueOf方法获取一个变量的Value信息 51 | ``` 52 | func ValueOf(i interface{}) Value 53 | ``` 54 | reflect.ValueOf(x),形参x被保存为一个接口值并作为参数传递(复制),方法内部把该接口值的值恢复出来保存为reflect.Value并返回。 55 | 56 | 接下来我们简单看一下Value类型: 57 | ``` 58 | type Value struct { 59 | // typ holds the type of the value represented by a Value. 60 | typ *rtype 61 | 62 | // Pointer-valued data or, if flagIndir is set, pointer to data. 63 | ptr unsafe.Pointer 64 | } 65 | ``` 66 | 可以看到Value结构体中保存两个字段: 67 | * typ:该变量的Type信息,调用Value的Type()方法将返回具体类型所对应的Type信息 68 | * ptr:该变量的值信息 69 | 70 | Interface方法是ValueOf方法的逆,它把一个reflect.Value恢复成一个接口值:把Value中保存的类型和值的信息打包成一个接口表示并返回。如: 71 | ``` 72 | //y的类型被断言为float64 73 | y, ok := v.Interface().(float64) 74 | ``` 75 | 76 | #### 通过反射调用方法 77 | 可以通过反射动态调用原对象的方法: 78 | ``` 79 | v := reflect.ValueOf(&x) 80 | m := v.MethodByName("Show") 81 | in := []reflect.Value{ 82 | reflect.ValueOf(23), 83 | reflect.ValueOf(323), 84 | } 85 | out := m.Call(in) //对于变参可用CallSlice方法 86 | ``` 87 | 88 | #### 通过反射修改对象 89 | 由于Go方法调用时是值传递,则通过ValueOf方法获取的Value对象是copy,即non-settable的,对它调用Set方法会出现错误。所以,如果想通过反射来修改对象,必须先把该对象的指针传给reflect.ValueOf(&x),然后通过Elem()方法就可以获得一个保存了原对象的Value对象,此时的Value对象就是settable的。 90 | ``` 91 | x := 1 92 | // non-settable 93 | d := reflect.ValueOf(x) 94 | d.CanSet() 95 | 96 | // settable 97 | d := reflect.ValueOf(&x).Elem() 98 | d.CanSet() 99 | d.Set(reflect.ValueOf(4)) 100 | d.SetInt(4) 101 | ``` 102 | 103 | 需要注意的是:虽然反射可以读取struct中未导出的成员,但不能修改这些未导出的成员,一个struct中只有被导出的字段才是settable的。 104 | -------------------------------------------------------------------------------- /6/6.1 总览.md: -------------------------------------------------------------------------------- 1 | ## 6.1 总览 2 | 3 | ### 知识面 4 | #### 基础数据结构 5 | string(支持数字类型)、list、set、zset和hash 6 | 7 | #### 常用命令 8 | 流水线:可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性 9 | 10 | 事务:watch、multi和exec命令结合(需要注意的是redis事务不具有原子性,前面失败后后面会继续执行而不是回滚)。所以一般需要使用watch命令配合:watch命令可用于监视key ,如果在事务执行之前这些 key 被其他命令所改动,那么事务将被打断 11 | 12 | 过期:expire 13 | 14 | #### 数据过期策略 15 | redis 提供6种数据淘汰策略: 16 | 1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 17 | 2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 18 | 3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 19 | 4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 20 | 5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 21 | 6. no-enviction(驱逐):禁止驱逐数据 22 | 23 | 注意这里的6种机制,volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。 24 |    25 | 使用策略规则: 26 | 1. 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru 27 | 2. 如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random 28 | 29 | #### 持久化 30 | 快照和aof 31 | 32 | #### replica 33 | 同步过程:第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录下来,待完成后将rdb文件全量同步到replica节点,replica节点将该rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到replica节点进行重放就完成了同步过程。 34 | 35 | 需要注意的是,redis中replica节点默认是不能对外提供服务,仅仅是一个冷备;也可以配置提供读功能以实现读写分离 36 | 37 | #### 集群 38 | Redis采用区中心的设计方案,通过虚拟16384个slot,将每个key映射到每一个具体的slot上,而每个redis节点可以负责管理一定数量的slot。 39 | 40 | ##### 客户端访问 41 | 在单机模式下,Redis对请求的处理很简单。Key存在的话,就执行请求中的操作;Key不存在的话,就告诉客户端Key不存在。然而在集群模式下,因为涉及到请求重定向和Slot迁移,所以对请求的处理变得很复杂,流程如下: 42 | 1. 首先检查key所在slot是否属于当前node:计算crc16(key) % 16384得到slot,查询clusterState.slots负责slot的结点指针,与myself指针比较 43 | 2. 若不属于,则响应MOVED错误重定向客户端 44 | 3. 若属于且key存在,则直接操作,返回结果给客户端 45 | 4. 若key不存在,检查该slot是否迁出中?(clusterState.migrating_slots_to) 46 | 5. 若slot迁出中,返回ASK错误重定向客户端到迁移目的服务器上 47 | 6. 若slot未迁出,检查slot是否导入中?(clusterState.importing_slots_from) 48 | 7. 若slot导入中且有ASKING标记,则直接操作 49 | 8. 否则响应MOVED错误重定向客户端 50 | 51 | #### 数据迁移 52 | 迁移数据的大致流程:首先需要确定哪些slot需要被迁移到目标节点,然后获取slot中key,将slot中的key全部迁移到目标节点,然后向集群所有主节点广播slot(数据)全部迁移到了目标节点。 53 | 54 | #### 单线程模型 55 | epoll模型,单线程处理客户端的连接和读写请求 56 | 57 | ### 常见应用 58 | 1. 分布式锁:超时特性(set命令)、锁的获取以及释放 59 | 2. 队列: 60 | 3. 延时队列:list和zset 61 | 62 | ### 常见问题 63 | 64 | #### 缓存和数据库双写一致性问题 65 | 一致性问题还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。 66 | 67 | 如果不要求强一致性,则可以首先采取正确更新策略,先更新数据库,再删缓存。其次因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。 68 | 69 | #### 缓冲穿透 70 | 一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,则去后端系统查找(比如DB)。如果key对应的value是不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。 71 | 1. 对查询结果为空的情况也进行缓存,缓存时间设置短一点。 72 | 2. 对不合法的key进行过滤,比如利用布隆过滤器key是否合法,不合法直接返回。 73 | 74 | #### 缓冲雪崩 75 | 大量的key在同一时间过期。 76 | 1. 一般需要在时间上加一个随机值,使得过期时间分散一些。 77 | 78 | #### 并发Key竞争问题 79 | 80 | #### 大key迁移 81 | 82 | -------------------------------------------------------------------------------- /7/7.1 Mysql总览.md: -------------------------------------------------------------------------------- 1 | ## 7.1 Mysql总览 2 | 3 | ### 基础架构 4 | 5 | ### 存储模型 6 | 目前广泛使用的是MyISAM和InnoDB两种引擎: 7 | 8 | #### MyISAM 9 | MyISAM引擎是MySQL 5.1及之前版本的默认引擎,它的特点是: 10 | 11 | 不支持行锁,读取时对需要读到的所有表加锁,写入时则对表加排它锁 12 | 13 | 不支持事务 14 | 15 | 不支持外键 16 | 17 | 不支持崩溃后的安全恢复 18 | 19 | 在表有读取查询的同时,支持往表中插入新纪录 20 | 21 | 支持BLOB和TEXT的前500个字符索引,支持全文索引 22 | 23 | 支持延迟更新索引,极大提升写入性能 24 | 25 | 对于不会进行修改的表,支持压缩表,极大减少磁盘空间占用 26 | 27 | #### InnoDB 28 | InnoDB在MySQL 5.5后成为默认索引,它的特点是: 29 | 30 | 支持行锁,采用MVCC来支持高并发 31 | 32 | 支持事务 33 | 34 | 支持外键 35 | 36 | 支持崩溃后的安全恢复 37 | 38 | 不支持全文索引 39 | 40 | 总体来讲,MyISAM适合SELECT密集型的表,而InnoDB适合INSERT和UPDATE密集型的表 41 | -------------------------------------------------------------------------------- /7/7.2 索引详解.md: -------------------------------------------------------------------------------- 1 | ## 7.2 索引详解 2 | 3 | ### 7.2.1 索引简介 4 | MySQL官方对索引的定义为:索引是帮助MySQL高效获取数据的数据结构。我们知道,查询是数据库的最主要功能,所以数据库系统的设计者会从查询算法的角度进行优化。比较常见的查找算法是顺序查找、二分查找、BST查找和哈希查找等。而每种查找算法都只能应用于特定的数据结构之上,例如二分查找要求被检索数据有序,而BST查找只能应用于二叉查找树上,哈希查找只能应用于哈希表。 5 | 6 | 但是数据本身的组织结构不可能完全满足各种查找要求(比如不可能同时将两列都按顺序进行组织),所以在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用数据,这样就可以在这些数据结构上实现快速的查找算法,即索引。 7 | 8 | 看一个例子: 9 | 10 | ![index-principle](../img/7-index-principle.png) 11 | 12 | 上图展示了一种可能的索引方式。左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上不一定物理相邻)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在O(log2n)的复杂度内获取到相应数据。 13 | 14 | ### 7.2.2 索引数据结构 15 | 索引是在MySQL的存储引擎层中实现的,而不是在服务层实现的。所以每种存储引擎的索引可能并不相同,也不是所有的存储引擎都支持所有的索引类型。这里我们主要讨论一下B-Tree索引。 16 | 17 | #### B+Tree 18 | 在介绍B-Tree索引之前,我们先来看一下相关数据结构:B+Tree,B+Tree是一种多叉平衡树,如下图所示: 19 | 20 | ![btree](../img/7-btree.jpg) 21 | 22 | 这里我们简单说一下其具体结构,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3。而P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。 23 | 24 | 如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。 25 | 26 | 最后我们看一下B+Tree性质: 27 | * 通过上面的分析,我们知道IO次数取决于B+Tree的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=log(m+1)N,当数据量N一定的情况下,m越大,h越小;而m=磁盘块的大小/数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么B+树要只把数据放到叶子节点,而索引字段尽量的小。 28 | * 当B+Tree的数据项是复合的数据结构(联合索引),比如(name,age,sex)的时候,B+数是按照从左到右的顺序来建立搜索树的,比如查询(Jack,20,F)这样的数据时,B+树会首先比较name来确定下一步的搜索方向,如果name相同再依次比较age和sex,最后得到检索的数据。但查询(20,F)这样的数据时,B+树就不知道首先该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。而查询(Jack,F)这样的数据时,B+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于Jack的数据都找到,然后再匹配性别是F的数据了,这个是非常重要的性质,即索引的最左前缀匹配特性。 29 | 30 | #### B-Tree索引 31 | 接下来我们看一下B-Tree索引,一般在数据库系统或B-Tree索引都在B+Tree的基础上进行了优化,增加了顺序访问指针。如下图所示: 32 | 33 | ![btree-index](../img/7-btree-index.png) 34 | 35 | 在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针,就形成了带有顺序访问指针的B+Tree。做这个优化的目的是为了提高区间访问的性能,例如如果要查询key为从18到49的所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。 36 | 37 | 我们首先回顾一下典型的mysql查询语句: 38 | ```sql 39 | SELECT * 40 | FROM table 41 | WHERE condition 42 | ORDER BY col 43 | LIMIT offset, num; 44 | ``` 45 | 其中WHERE和ORDER BY子句可以使用索引来优化。我们首先看一下WHERE子句: 46 | * 等值匹配:可用于= != <> IN NOT IN等查询语句的优化 47 | * 范围匹配:可用于 > >= < <= BTEWEEN AND等范围查询语句的优化 48 | * 最左前缀匹配:对于name like bai%这种后模糊匹配的查询,是可以利用name字段上建立的索引来优化查询的,但是对于name like %bai这种前模糊匹配的查询则不能使用索引的 49 | * 联合索引匹配: 50 | 51 | 排序操作也是可以使用索引来进行,这样可以大大提高排序的速度,要使用索引来排序需要满足以下两点即可: 52 | * ORDER BY子句后的列顺序要与联合索引的列顺序一致,且所有排序列的排序方向需要一致(递增或者递减) 53 | * 所查询的字段值需要包含在索引列中,即满足覆盖索引 54 | 55 | 通过例子来具体分析,在user_info表上创建一个联合索引`ALTER TABLE user_info ADD INDEX index_user(user_name, city, age);`,我们先看一些可以使用到索引排序的例子: 56 | ```sql 57 | SELECT user_name, city, age FROM user_info ORDER BY user_name; 58 | SELECT user_name, city, age FROM user_info ORDER BY user_name, city; 59 | SELECT user_name, city, age FROM user_info ORDER BY user_name DESC, city DESC; 60 | SELECT user_name, city, age FROM user_info WHERE user_name = 'Jack' ORDER BY city; 61 | ``` 62 | 最后一个特殊一点:如果where查询条件为索引列的第一列,且为常量条件,那么该排序也是可以使用索引的。 63 | 64 | 接下来我们看一些无法使用索引排序的案例 65 | ```sql 66 | //sex不在索引列中 67 | SELECT user_name, city, age FROM user_info ORDER BY user_name, sex; 68 | //排序列的方向不一致 69 | SELECT user_name, city, age FROM user_info ORDER BY user_name ASC, city DESC; 70 | //所要查询的字段列sex没有包含在索引列中 71 | SELECT user_name, city, age, sex FROM user_info ORDER BY user_name; 72 | //where查询条件后的user_name为范围查询 73 | SELECT user_name, city, age FROM user_info WHERE user_name LIKE 'Jack%' ORDER BY city; 74 | ``` 75 | 76 | 而多表连接查询时,只有当ORDER BY后的排序字段都是第一个表中的索引列(需要满足以上索引排序的两个规则)时,方可使用索引排序。如:再创建一个用户的扩展表user_info_ext,并建立uid的索引。 77 | ```sql 78 | CREATE TABLE user_info_ext( 79 | id int AUTO_INCREMENT PRIMARY KEY, 80 | uid int NOT NULL, 81 | u_password VARCHAR(64) NOT NULL 82 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 83 | 84 | ALTER TABLE user_info_ext ADD INDEX index_user_ext(uid); 85 | ``` 86 | 则以下两个查询排序,第一个可以走索引排序,第二个不会走索引排序: 87 | ```sql 88 | //走索引排序 89 | SELECT user_name, city, age FROM user_info u LEFT JOIN user_info_ext ue ON u.id = ue.uid ORDER BY u.user_name; 90 | //不走索引排序 91 | SELECT user_name, city, age FROM user_info u LEFT JOIN user_info_ext ue ON u.id = ue.uid ORDER BY ue.uid; 92 | ``` 93 | 94 | #### 聚集索引&非聚集索引 95 | ##### 聚集索引 96 | 聚集索引并不是一种单独的索引类型,而是一种数据存储方式,InnoDB的聚集索引实际上是将主键与数据行存放在同一个文件中。而普通索引(二级索引)存放的是主键的值,所以使用普通索引查询时需要两次查询:首先找到对应的主键值,然后再根据主键值去聚集索引中查询到对应的数据。如下图所示: 97 | 98 | ![innodb-pkey](../img/7-innodb-pkey.png) 99 | 100 | ![innodb-skey](../img/7-innodb-skey.png) 101 | 102 | InnoDB将主键与数据聚集在一起的方式,使得按主键顺序的插入和查询效率会很高,但是更新主键的字段或者不按主键的顺序插入数据的代价会比较高,所以主键的选取很重要(使用AUTO INCREMENT字段或者应用程序生成的顺序递增字段要比无序的UUID好的多)。而二级索引会保存主键的值,所以主键的值不要太大。 103 | 104 | ##### 非聚集索引 105 | 非聚集索引的索引与数据是存在在不同文件的,对于MyISAM引擎的一张表,会有三种文件:FRM(表结构)、MYD(数据,就是数据库中的每个行)、MYI(索引)。MySQL使用索引查询数据时,先到MYI文件中找出数据存储的位置指针,然后再到MYD文件中读取数据。如下图所示: 106 | 107 | ![myisam-index](../img/7-myisam-index.png) 108 | 109 | ### 7.2.4 高效索引策略 110 | 通过上文,相信你对B+Tree的数据结构已经有了大致的了解,但MySQL中索引是如何组织数据的存储呢?以一个简单的示例来说明,假如有如下数据表: 111 | ```sql 112 | CREATE TABLE People( 113 | last_name varchar(50) not null, 114 | first_name varchar(50) not null, 115 | dob date not null, 116 | gender enum(`m`,`f`) not null, 117 | key(last_name,first_name,dob) 118 | ); 119 | ``` 120 | 对于表中每一行数据,索引中包含了last_name、first_name、dob列的值,下图展示了索引是如何组织数据存储的。 121 | 122 | ![unified-index](../img/7-unified-index.webp) 123 | 124 | 可以看到,索引首先根据第一个字段来排列顺序,当last_name相同,则根据first_name,最后根据第三个字段dob来排序,正是因为这个原因,才有了索引的“最左原则”。 125 | 126 | #### 非独立的列 127 | “独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。比如:`select * from where id + 1 = 5;`,我们很容易看出其等价于 id = 4,但是MySQL无法自动解析这个表达式,使用函数是同样的道理。 128 | 129 | #### 前缀索引 130 | 对于字符列,可以使用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时大大节约索引空间。对于BLOB、TEXT、或者很长的VARCHAR类型的列,必须使用前缀索引。 131 | 132 | #### 多列条件和联合索引 133 | 当查询条件为多个时,在多个列上建立独立的索引并不能提高查询性能,在老版本,比如MySQL5.0之前就会随便选择一个索引,而新的版本会采用索引合并的策略。举个简单的例子,在一张电影演员表中,在actor_id和film_id两个列上都建立了独立的索引,然后有如下查询:`select film_id,actor_id from film_actor where actor_id = 1 or film_id = 1;`老版本的MySQL会随机选择一个索引,但新版本做如下的优化: 134 | ```sql 135 | select film_id,actor_id from film_actor where actor_id = 1 136 | union all 137 | select film_id,actor_id from film_actor where film_id = 1 and actor_id <> 1 138 | ``` 139 | 当出现多个索引做相交操作时(多个AND条件),通常来说一个包含所有相关列的索引要优于多个独立索引。 140 | 当出现多个索引做联合操作时(多个OR条件),对结果集的合并、排序等操作需要耗费大量的CPU和内存资源,特别是当其中的某些索引的选择性不高,需要返回合并大量数据时,查询成本更高。所以这种情况下还不如走全表扫描。 141 | 142 | 因此explain时如果发现有索引合并时(Extra字段出现Using union),我们应该考虑索引是否合适,是不是建立一个包含所有相关列的联合索引更适合。 143 | 144 | 前面我们提到过索引如何组织数据存储的,从图中可以看到联合索引时,索引的顺序对于查询是至关重要的,很明显应该把选择性更高的字段放到索引的前面。索引选择性是指不重复的索引值和数据表的总记录数的比值,选择性越高查询效率越高。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。 145 | 146 | 执行下面的查询,越接近1,则说明该字段的选择性越好,则放在前面更合适。 147 | ```sql 148 | select count(distinct actor_id)/count(*) as actor_id_selectivity, 149 | count(distinct film_id)/count(*) as film_id_selectivity 150 | from film_actor; 151 | ``` 152 | 153 | #### 避免多个范围条件 154 | 实际开发中,我们会经常使用多个范围条件,比如想查询某个时间段内登录过的用户: 155 | ```sql 156 | select user.* from user where login_time > '2017-04-01' and age between 18 and 30; 157 | ``` 158 | 这个查询有一个问题:它有两个范围条件,login_time列和age列,MySQL可以使用login_time列的索引或者age列的索引,但无法同时使用它们。 159 | 160 | #### 覆盖索引 161 | 如果一个索引包含所有需要查询字段的值,我们就称之为覆盖索引,覆盖索引能够极大的提高性能,其不需要再回表查询相关数据。当发起一个被索引覆盖的查询时,在EXPLAIN的Extra列可以看到“Using index”的信息。 162 | 163 | #### 冗余和重复索引 164 | 冗余索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应当尽量避免这种索引,发现后立即删除。比如有一个索引(A,B),再创建索引(A)就是冗余索引。冗余索引经常发生在为表添加新索引时,比如有人新建了索引(A,B),但这个索引不是扩展已有的索引(A)。 165 | 大多数情况下都应该尽量扩展已有的索引而不是创建新索引。但有极少情况下出现性能方面的考虑需要冗余索引,比如扩展已有索引而导致其变得过大,从而影响到其他使用该索引的查询。 166 | 167 | ### 7.2.4 索引操作 168 | #### 创建 169 | 在执行CREATE TABLE语句时可以创建索引,也可以单独用CREATE INDEX或ALTER TABLE来为表增加索引。 170 | ```sql 171 | //CREATE TABLE 172 | CREATE TABLE table_name( 173 | column_name data_type, 174 | ...... 175 | [UNIQUE|FULLTEXT|SPATIAL] {INDEX|KEY} index_name [USING {BTREE | HASH}] (col_name [(length)] [ASC | DESC]...) 176 | ); 177 | 178 | //ALTER TABLE 179 | ALTER TABLE table_name ADD [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name [USING {BTREE | HASH}] (col_name [(length)] [ASC | DESC]...) 180 | ALTER TABLE table_name ADD PRIMARY KEY (col_name [(length)] [ASC | DESC]..) 181 | 182 | //CREATE INDEX 183 | CREATE [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name [USING {BTREE | HASH}] ON tbl_name (col_name [(length)] [ASC | DESC],...) 184 | ``` 185 | #### 删除 186 | ```sql 187 | DROP INDEX index_name ON talbe_name 188 | ALTER TABLE table_name DROP INDEX index_name 189 | ``` -------------------------------------------------------------------------------- /7/7.3 查询详解.md: -------------------------------------------------------------------------------- 1 | ## 7.3 查询详解 2 | 3 | ### 7.3.1 查询执行 4 | ### 7.3.2 查询优化 5 | 6 | #### 优化COUNT()查询 7 | COUNT()有两种不同的作用,其一是统计某个列值的数量,其二是统计行数。统计列值时,要求列值非空,它不会统计NULL,如果确认括号中的表达式不为NULL时,实际上就是在统计行数。我们最常见的误解也就在这儿,在括号内指定了一列却希望统计结果是行数,而且还常常误以为前者的性能会更好。但实际并非这样,如果要统计行数,直接使用COUNT(*),意义清晰,且性能更好。 8 | 9 | 有时候某些业务场景并不需要完全精确的COUNT值,可以用近似值来代替,EXPLAIN出来的行数就是一个不错的近似值,而且执行EXPLAIN并不需要真正地去执行查询,所以成本非常低。通常来说,执行COUNT()都需要扫描大量的行才能获取到精确的数据,因此很难优化,MySQL层面还能做得也就只有覆盖索引了。如果不还能解决问题,只有从架构层面解决了,比如添加汇总表,或者使用redis这样的外部缓存系统。 10 | 11 | #### 优化关联查询 12 | 在大数据场景下,表与表之间通过一个冗余字段来关联,要比直接使用JOIN有更好的性能。如果确实需要使用关联查询的情况下,需要特别注意的是:确保ON和USING字句中的列上有索引,而且在创建索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候,如果优化器关联的顺序是A、B,那么就不需要在A表的对应列上创建索引。一般来说,除非有其他理由,只需要在关联顺序中的第二张表的相应列上创建索引。 13 | 14 | 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化。 15 | 16 | 要理解第一点,我们首先看一下MySQL是如何执行关联查询的:它对任何的关联查询都执行嵌套循环关联操作,即先在一个表中循环取出单条数据,然后在嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为为止。然后根据各个表匹配的行,返回查询中需要的各个列。我们这里以内连接为例简单说明一下(之后我们会详细分析连接): 17 | ```sql 18 | SELECT tbl1.col1, tbl2.col2 19 | FROM tbl1 20 | INNER JOIN tbl2 21 | ON tbl1.col3 = tbl2.col3 22 | WHERE tbl1.col1 IN(5, 6); 23 | 24 | outer_iter = iterator over tbl1 where col1 IN(5, 6) 25 | outer_row = outer_iter.next 26 | while outer_row 27 | inner_iter = iterator over tbl2 where col3 = outer_row.col3 28 | inner_row = inner_iter.next 29 | while inner_row 30 | output(outer_row.col1, inner_row.col2) 31 | inner_row = inner_iter.next 32 | end 33 | outer_row = outer_iter.next 34 | end 35 | ``` 36 | 可以看到,最外层的查询是根据tbl1.col1列来查询的,即使tbl1.col3上有索引,这个查询也是不会使用的。再看内层的查询,很明显tbl2.col3上如果有索引的话,能够加速查询,因此只需要在关联顺序中的第二张表的相应列上创建索引即可。 37 | 38 | #### 优化LIMIT分页 39 | 当需要分页操作时,通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY子句。如果有对应的索引,通常效率会不错,否则MySQL需要做大量的文件排序操作。 40 | 41 | 一个常见的问题是当偏移量非常大时,比如:`LIMIT 10000, 10`这样的查询,MySQL需要查询10010条记录然后只返回10条记录,前面的10000条都将被抛弃,这样的代价非常高。 42 | 43 | 优化这种查询一个最简单的办法是首先使用覆盖索引扫描出主键id,而不是查询所有的列。然后根据主键id做范围查询进行分页,例如下面的查询: 44 | ```sql 45 | SELECT * FROM film ORDER BY title LIMIT 10000, 10; 46 | ``` 47 | 可以考虑改写为 48 | ```sql 49 | SELECT film.* 50 | FROM film JOIN ( 51 | SELECT id FROM film ORDER BY title LIMIT 10000, 10 52 | ) AS tmp USING(id); 53 | ``` 54 | 如果主键id是递增的,还可以改写为以下这种: 55 | ```sql 56 | SELECT * 57 | FROM film 58 | WHERE id > (SELECT id from film ORDER BY title LIMIT 10000, 1) 59 | ORDER BY id 60 | LIMIT 10; 61 | ``` 62 | 63 | 如果client端可以保存上次取数据的位置id,那么下次就可以直接从该id开始扫描,即`SELECT * FROM film WHERE id > 10000 LIMIT 10;`。 64 | 65 | #### 优化UNION 66 | MySQL处理UNION的策略是先创建临时表,然后再把各个查询结果插入到临时表中,最后再来做查询。因此很多优化策略在UNION查询中都没有办法很好的时候。经常需要手动将WHERE、LIMIT、ORDER BY等字句“下推”到各个子查询中,以便优化器可以充分利用这些条件先优化。 67 | 68 | 除非确实需要Mysql去重,否则就一定要使用UNION ALL,如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致整个临时表的数据做唯一性检查,这样做的代价非常高。当然即使使用ALL关键字,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。虽然很多时候没有这个必要,比如有时候可以直接把每个子查询的结果返回给客户端。 69 | 70 | ### 7.3.3 Explain 71 | MySQL提供了一个EXPLAIN命令,它可以对SELECT语句进行分析,并输出SELECT执行的详细信息,以供开发人员针对性优化。EXPLAIN命令用法十分简单,在SELECT语句前加上Explain就可以了,例如: 72 | ```sql 73 | EXPLAIN SELECT * from user_info WHERE id < 300; 74 | ``` 75 | 76 | #### 准备 77 | 为了接下来方便演示EXPLAIN的使用,首先我们需要建立两个测试用的表,并添加相应的数据: 78 | ```sql 79 | CREATE TABLE user_info ( 80 | id BIGINT(20) NOT NULL AUTO_INCREMENT, 81 | name VARCHAR(50) NOT NULL DEFAULT '', 82 | age INT(11) NOT NULL DEFAULT -1, 83 | PRIMARY KEY (id), 84 | KEY name_index (name) 85 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8 86 | 87 | INSERT INTO user_info (name, age) VALUES ('Jack', 20); 88 | ... 89 | 90 | CREATE TABLE order_info ( 91 | id BIGINT(20) NOT NULL AUTO_INCREMENT, 92 | user_id BIGINT(20) NOT NULL DEFAULT -1, 93 | product_name VARCHAR(50) NOT NULL DEFAULT '', 94 | productor VARCHAR(30) NOT NULL DEFAULT -1, 95 | PRIMARY KEY (id), 96 | KEY user_product_detail_index (user_id, product_name, productor) 97 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8 98 | 99 | INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p1', 'WHH'); 100 | ... 101 | ``` 102 | 103 | EXPLAIN命令的输出内容大致如下: 104 | ```sql 105 | mysql> explain select * from user_info where id = 2; 106 | *************************** 1. row *************************** 107 | id: 1 108 | select_type: SIMPLE 109 | table: user_info 110 | partitions: NULL 111 | type: const 112 | possible_keys: PRIMARY 113 | key: PRIMARY 114 | key_len: 8 115 | ref: const 116 | rows: 1 117 | filtered: 100.00 118 | Extra: NULL 119 | 1 row in set, 1 warning (0.00 sec) 120 | ``` 121 | 我们简单看一下各列的含义如下: 122 | * id: SELECT查询的标识符,每个SELECT查询都会自动分配一个唯一的标识符 123 | * select_type: SELECT查询的类型 124 | * table: 查询的是哪个表 125 | * partitions: 匹配的分区 126 | * type: 访问类型 127 | * possible_keys: 此次查询中可能选用的索引 128 | * key: 此次查询中确切使用到的索引 129 | * ref: 哪个字段或常数与key一起被使用 130 | * rows: 显示此查询一共扫描了多少行,这个是一个估计值 131 | * filtered: 表示此查询条件所过滤的数据的百分比 132 | * extra: 额外的信息 133 | 134 | 接下来我们来重点看一下比较重要的几个字段 135 | 136 | #### type 137 | type字段非常重要,它提供了判断查询是否高效的重要依据依据。通过type字段,我们判断此次查询是全表扫描还是索引扫描等。 138 | 139 | type常用的取值有: 140 | * system:表中只有一条数据,这个类型是特殊的const类型 141 | * const:针对主键或唯一索引的等值查询,最多只返回一行数据。例如下面的这个查询就是const类型的,它使用了主键索引: 142 | ```sql 143 | mysql> explain select * from user_info where id = 2; 144 | *************************** 1. row *************************** 145 | id: 1 146 | select_type: SIMPLE 147 | table: user_info 148 | partitions: NULL 149 | type: const 150 | possible_keys: PRIMARY 151 | key: PRIMARY 152 | key_len: 8 153 | ref: const 154 | rows: 1 155 | filtered: 100.00 156 | Extra: NULL 157 | 1 row in set, 1 warning (0.00 sec) 158 | ``` 159 | * eq_ref:使用这种索引查找,mysql知道最多只匹配一行记录。当我们使用主键索引或者唯一索引的时候,且这个索引的所有组成部分都被用上,才能是该类型。此类型通常出现在多表的join查询,表示对于前表的每一个结果,都只能匹配到后表的一行记录。并且查询的比较操作通常是=,查询效率较高。例如: 160 | ```sql 161 | mysql> EXPLAIN SELECT * FROM user_info, order_info WHERE user_info.id = order_info.user_id; 162 | *************************** 1. row *************************** 163 | id: 1 164 | select_type: SIMPLE 165 | table: order_info 166 | partitions: NULL 167 | type: index 168 | possible_keys: user_product_detail_index 169 | key: user_product_detail_index 170 | key_len: 314 171 | ref: NULL 172 | rows: 9 173 | filtered: 100.00 174 | Extra: Using where; Using index 175 | *************************** 2. row *************************** 176 | id: 1 177 | select_type: SIMPLE 178 | table: user_info 179 | partitions: NULL 180 | type: eq_ref 181 | possible_keys: PRIMARY 182 | key: PRIMARY 183 | key_len: 8 184 | ref: test.order_info.user_id 185 | rows: 1 186 | filtered: 100.00 187 | Extra: NULL 188 | 2 rows in set, 1 warning (0.00 sec) 189 | ``` 190 | * ref:此类型是一种索引查找,它返回所有匹配单个值的行,和eq_ref的区别在于,它可能会找到多个符合条件的行,一般会在使用非唯一性索引或者唯一性索引的非唯一性前缀时看到,把它叫做ref是因为索引要和某个参考值比较,这个参考值或者是一个常数,或者是来自多表联合查询前一个表的结果值。如下所示: 191 | ```sql 192 | mysql> EXPLAIN SELECT * FROM user_info, order_info WHERE user_info.id = order_info.user_id AND order_info.user_id = 5; 193 | *************************** 1. row *************************** 194 | id: 1 195 | select_type: SIMPLE 196 | table: user_info 197 | partitions: NULL 198 | type: const 199 | possible_keys: PRIMARY 200 | key: PRIMARY 201 | key_len: 8 202 | ref: const 203 | rows: 1 204 | filtered: 100.00 205 | Extra: NULL 206 | *************************** 2. row *************************** 207 | id: 1 208 | select_type: SIMPLE 209 | table: order_info 210 | partitions: NULL 211 | type: ref 212 | possible_keys: user_product_detail_index 213 | key: user_product_detail_index 214 | key_len: 9 215 | ref: const 216 | rows: 1 217 | filtered: 100.00 218 | Extra: Using index 219 | 2 rows in set, 1 warning (0.01 sec) 220 | ``` 221 | * range:表示使用索引范围查询,通过索引字段范围获取表中部分数据记录,这个类型通常出现在`=, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN AND, IN()`操作中。当type是range时,那么EXPLAIN输出的ref字段为NULL,并且key_len字段是此次查询中使用到的索引的最长的那个。例如下面的例子就是一个范围查询: 222 | ```sql 223 | mysql> EXPLAIN SELECT * FROM user_info WHERE id BETWEEN 2 AND 8; 224 | *************************** 1. row *************************** 225 | id: 1 226 | select_type: SIMPLE 227 | table: user_info 228 | partitions: NULL 229 | type: range 230 | possible_keys: PRIMARY 231 | key: PRIMARY 232 | key_len: 8 233 | ref: NULL 234 | rows: 7 235 | filtered: 100.00 236 | Extra: Using where 237 | 1 row in set, 1 warning (0.00 sec) 238 | ``` 239 | * index:表示全索引扫描,和ALL类型类似,只不过mysql扫描表时按照索引次序进行而不是数据行。它的优点是避免了排序,缺点是要承担按照索引次序读取整个表的开销(即大量的随机IO,开销很大)。如果在Extra字段显示“Using index”,说明本次查询可以使用覆盖索引,则只扫描索引的数据,而不会扫描具体的数据行,性能会好很多。例如: 240 | ```sql 241 | mysql> EXPLAIN SELECT name FROM user_info; 242 | *************************** 1. row *************************** 243 | id: 1 244 | select_type: SIMPLE 245 | table: user_info 246 | partitions: NULL 247 | type: index 248 | possible_keys: NULL 249 | key: name_index 250 | key_len: 152 251 | ref: NULL 252 | rows: 10 253 | filtered: 100.00 254 | Extra: Using index 255 | 1 row in set, 1 warning (0.00 sec) 256 | ``` 257 | 上面的例子中,我们查询的name字段恰好是一个索引,因此我们可以直接从索引中获取数据,而不需要再回表查询具体的数据行。 258 | * ALL:表示全表扫描,这个类型的查询是性能最差的查询,mysql必须从头到尾扫描整张表来找到需要的行。一般可以通过对相应的字段添加索引来避免。 259 | 260 | type类型的性能比较 261 | 通常来说,不同的type类型的性能关系如下: 262 | ALL < index < range ~ index_merge < ref < eq_ref < const < system 263 | 264 | #### possible_keys 265 | possible_keys表示MySQL在查询时可以使用哪些索引,这是基于查询访问的列和使用的比较操作符判断的。 266 | 267 | #### key 268 | key表示mysql决定采用哪个索引来优化对该表的访问。 269 | 270 | #### key_len 271 | 表示查询优化器使用了索引的字节数. 这个字段可以评估组合索引是否完全被使用, 或只有最左部分字段被使用到. 272 | 273 | #### rows 274 | rows也是一个非常重要的字段,它是MySQL估计为了找到结果集需要读取的行数,而且这个数字是内嵌关联计划里的循环数目。通过将所有rows列相乘,可以粗略地估算出本次查询需要检查的行数。 275 | 276 | #### Extra 277 | Explain中的很多额外的信息会在Extra字段显示,此字段能够给出让我们深入理解执行计划进一步的细节信息,我们简单介绍一下常见的几个: 278 | * Using where:在查找使用索引的情况下,需要回表去查询所需的数据。 279 | * Using index:表示本次查询将会使用覆盖索引,以避免访问表。 280 | * Using filesort:当SQL中包含ORDER BY操作,而且无法利用索引完成排序操作的时候,mysql将进行外部排序。filesort只能应用在单个表上,如果有多个表的数据需要排序,那么MySQL会先使用using temporary保存临时数据,然后再在临时表上使用filesort进行排序,最后输出结果。 281 | * Using temporary:查询有使用临时表,一般出现于排序,分组和多表join的情况,一般需要优化。 -------------------------------------------------------------------------------- /7/7.4 事务详解.md: -------------------------------------------------------------------------------- 1 | ## 7.4 事务详解 2 | 3 | ### 7.4.1 事务 4 | 事务是数据库永恒不变的话题,事务有4个特性,即ACID:原子性,一致性,隔离性,持久性。 5 | * 原子性(atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作 6 | * 一致性(consistency):数据库总是从一个一致性的状态转换到另外一个一致性的状态。 7 | * 隔离性(isolation):一个事务所做的修改在最终提交以前,对其他事务是不可见的。 8 | * 持久性(durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。 9 | 10 | 这四个特性,最重要的就是一致性,而一致性由原子性,隔离性,持久性来保证: 11 | * 原子性由Undo Log保证,Undo Log会保存每次变更之前的记录,从而在发生错误时进行回滚。 12 | * 隔离性由Lock和MVCC保证,这个比较复杂,我们之后会详细讨论。 13 | * 持久性由Redo Log保证,每次真正修改数据之前,都会首先写到Redo Log中,只有Redo Log写入成功,才会真正的写入到磁盘中,如果提交之前断电,就可以通过Redo Log恢复记录。 14 | 15 | 关于隔离性,SQL标准定义了4种隔离级别: 16 | * 未提交读(RU):事务中的修改,即使没有提交,对其他事务也都是可见的,其他事务可以读取该事务未提交的数据,也被称为脏读(Dirty Read),这个级别会导致很多问题。 17 | * 已提交读(RC):Oracle的默认隔离级别,一个事务开始时,只能“看见”已经提交的事务所做的修改。即一个事务从开始直到提交之前,所做的任何修改对其他事务是不可见的。RC有时间也叫不可重复读(nonrepeatable read),因为一次事务中先后执行两次相同的查询,可能得到不同的结果。 18 | * 可重复读(RR):MySql的默认隔离级别,该级别保证在同一个事务中多次读取同样记录的结果一致,该级别会有幻读的可能。 19 | * 串行化(serializable):事务串行执行,一般不会采用。 20 | 21 | 每个级别都会解决不同的问题,具体如下图所示: 22 | 23 | ![sql-isolation](../img/7-sql-isolation.jpg) 24 | 25 | 原理剖析: 26 | * RU发生脏读的原因:RU是对每个更新语句的行记录进行加锁,而不是对整个事务进行加锁,所以会发生脏读。而RC和RR会对整个事务加锁。 27 | * RC不能重复读的原因:RC是事务中每条Select语句都会生成一个新的Read View,所以每次读到的都是不同的。而RR是事务开启后会创建一个Read View,整个事务过程中会使用该Read view,所以从始至终都是使用同一个Read View。 28 | 29 | ### 7.4.2 锁 30 | 锁机制就是数据库为了保证数据的一致性而使各种共享资源在被并发访问变得有序所设计的一种规则。 31 | 32 | 按封锁类型分类(数据对象可以是表可以是数据行): 33 | * 排他锁(又称写锁,X锁):会阻塞其他事务的读和写操作。 34 | * 共享锁(又称读取,S锁):会阻塞其他事务的写操作。 35 | 36 | 按封锁的数据粒度分类如下: 37 | * 行级锁定(row-level):行级锁的开销大,加锁慢,会出现死锁;但是锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 38 | * 表级锁定(table-level):表级锁的开销小,加锁快,不会出现死锁;但是锁定粒度大,发生锁冲突的概率最高,并发度最低。 39 | 40 | InnoDB的行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过再数据块中,对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则InnoDB将使用表锁,在实际开发中应当注意。 41 | 42 | 当我们用范围条件检索数据时,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。 43 | 44 | 例子:假如user_info表中只有101条记录,其user_id的值分别是1,2,...,100,则查询语句`Select * from user_info where user_id >= 100 for update;`是一个范围查询,InnoDB不仅会对符合条件的user_id值为100的记录加锁,也会对user_id大于100(这些记录并不存在)的“间隙”加锁。 45 | 46 | InnoDB使用间隙锁的目的有两个: 47 | * 为了防止幻读(RR隔离级别下再通过GAP锁即可避免了幻读) 48 | * 满足恢复和复制的需要:在Mysql中,一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读 49 | 50 | ### 7.4.3 MVCC 51 | 首先我们看一下MVCC的基本原理是:MVCC的实现,通过保存数据在某个时间点的快照来实现的,这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。 52 | 53 | 我们看一下MVCC的理想模型:在每一行数据中额外保存两个隐藏的列:当前行创建时的版本号和删除时的版本号(可能为空)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较。这样事务在执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的。MVCC具体的操作如下: 54 | * SELECT:DB会根据以下两个条件检查每行记录:(1)DB只查找版本早于当前事务版本的数据行(也就是,数据行的版本号小于或等于当前事务的版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的;(2)行的删除版本号要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。只有1和2同时满足的记录,才能做为查询结果。 55 | * INSERT:DB为新插入的每一行保存当前系统版本号作为行版本号。 56 | * DELETE:DB为删除的每一行保存当前系统版本号作为行删除标识。 57 | * UPDATE:DB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当系统的版本号为原来的行作为删除标识。 58 | 59 | 保存这两个额外系统版本号,使大多数操作都可以不用加锁。 60 | 61 | #### 当前读和快照读 62 | InnoDB中的操作可以分为快照读(snapshot read)和当前读(current read): 63 | * 快照读(snapshot read):简单的select操作(当然不包括`select ... lock in share mode, select ... for update`) 64 | * 当前读(current read):如下所示: 65 | ```sql 66 | select ... lock in share mode 67 | select ... for update 68 | insert 69 | update 70 | delete 71 | ``` 72 | 在RR级别下,快照读是通过MVCC和Undo Log来实现的,而当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。由于两种读的实现机制不同导致一个事务中同时穿插快照读和当前读,是会可能出现幻读的。比如事务B在事务A执行中insert了一条数据并提交,事务A再次查询,普通的select读仍然是undo中的旧版本数据,但是对该行记录执行update或者delete(当前读)却是可以成功的(这也是第二类更新丢失问题)。 73 | 74 | #### InnoDB实现方式 75 | InnoDB存储引擎在数据库每行数据的后面添加了三个字段: 76 | * 6字节的事务ID(DB_TRX_ID)字段:用来标识本数据行最近一次修改(insert|update)的事务的事务id。至于delete操作,在InnoDB看来也不过是一次update操作,更新行中的一个特殊位将行标识为deleted,并非真正删除。 77 | * 7字节的回滚指针(DB_ROLL_PTR)字段:指写入回滚段(rollback segment)的Undo Log record (撤销日志记录记录)。如果一行记录被更新,则Undo Log record包含“重建该行记录被更新之前内容”所必须的信息。 78 | * 6字节的DB_ROW_ID字段:包含一个随着新行插入而单调递增的行ID,一般也就是主键ID。 79 | 80 | InnoDB的MVCC就是用Undo Log链表实现:事务以排它锁的方式修改原始数据,把修改前的数据存放于Undo Log,通过回滚指针与数据关联,如果修改成功,什么都不做,如果修改失败,则恢复Undo Log中的数据。如下图所示: 81 | 82 | ![InnoDB-mvcc](../img/7-innodb-mvcc.png) 83 | 84 | #### 总结 85 | 需要注意的是:MVCC只在RR和RC两个隔离级别下工作,RU总是读取到最新的数据行,不符合当前事务版本的数据行,而Serializable则会对所有读取的行加锁。 86 | 87 | 一般我们认为MVCC有下面几个特点: 88 | * 每行数据都存在一个版本,每次数据更新时都更新该版本 89 | * 修改时Copy出当前版本,然后随意修改,各个事务之间无干扰 90 | * 保存时比较版本号,如果成功(commit),则覆盖原记录,失败则放弃copy(rollback) 91 | * 就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道,因为这看起来正是,在提交的时候才能知道到底能否提交成功 92 | 93 | 而InnoDB实现MVCC的方式是: 94 | * 事务以排他锁的形式修改原始数据 95 | * 把修改前的数据存放于Undo Log,通过回滚指针与主数据关联 96 | * 修改成功(commit)啥都不做,失败则恢复Undo Log中的数据(rollback) 97 | 98 | 二者最本质的区别是:当修改数据时是否要排他锁定。所以InnoDB的实现并不是纯粹的MVCC,其并没有实现核心的多版本共存,Undo Log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。 99 | 100 | 理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,可以通过比较版本号进行提交或者回滚。但当事务影响到多行数据时,理想的MVCC就比较麻烦了:比如事务A执行理想的MVCC,修改Row1成功但修改Row2失败,此时需要回滚Row1,但因为Row1并没有被锁定,其数据可能又被事务B所修改,如果此时回滚Row1的内容,则会破坏事务B的修改结果,导致事务B违反ACID,这也正是所谓的第一类更新丢失的情况。也正是因为InnoDB使用的MVCC中结合了排他锁,所以不会出现第一类更新丢失,一般说更新丢失都是指第二类更新丢失。 101 | 102 | 理想MVCC难以实现的根本原因在于企图通过乐观锁代替两阶段段提交。在要求一致性的情况下,修改两行数据与修改两个分布式系统中的数据并无区别,而两阶段提交是目前这种场景保证一致性的唯一手段。两阶段提交的本质是锁定,乐观锁的本质是消除锁定,这二者是矛盾的。在MVCC方面,InnoDB只是提供了读的非阻塞。 103 | -------------------------------------------------------------------------------- /7/7.5 连接详解.md: -------------------------------------------------------------------------------- 1 | ## 7.5 连接详解 2 | 3 | ### 连接 4 | 连接的主要作用是根据两个或多个表中的列之间的关系,获取存在于不同表中的数据。连接分为三类:内连接、外连接、全连接。 5 | 6 | ![sql-join](../img/7-sql-join-summary.jpg) 7 | 8 | 在分析这三类连接之前,我们先准备一下数据: 9 | ```sql 10 | CREATE DATABASE join_test CHARSET UTF8; 11 | 12 | CREATE TABLE `Persons` ( 13 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 14 | `lastName` char(16) NOT NULL DEFAULT '', 15 | `firstName` char(16) NOT NULL DEFAULT '', 16 | `address` varchar(128) NOT NULL DEFAULT '', 17 | `city` varchar(128) NOT NULL DEFAULT '', 18 | PRIMARY KEY (`id`) 19 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 20 | 21 | CREATE TABLE `Orders` ( 22 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 23 | `orderNo` int(11) NOT NULL DEFAULT '0', 24 | `pid` int(11) NOT NULL DEFAULT '0', 25 | PRIMARY KEY (`id`) 26 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 27 | 28 | INSERT INTO `Persons` (`lastName`, `firstName`, `address`, `city`) 29 | VALUES 30 | ('Adams', 'John', 'Oxford Street', 'London'), 31 | ('Bush', 'George', 'Fifth Avenue', 'New York'), 32 | ('Carter', 'Thomas', 'Changan Street', 'Beijing'); 33 | 34 | INSERT INTO `Orders` (`orderNo`, `pid`) 35 | VALUES (77895, 3), (44678, 3), (22456, 1), (24562, 1), (34764, 65); 36 | ``` 37 | 则Persons表如下图所示: 38 | 39 | ![sql-person](../img/7-sql-person.jpg) 40 | 41 | Orders表如下图所示: 42 | 43 | ![sql-order](../img/7-sql-order.jpg) 44 | 45 | 接下来我们简单来讨论一下这三种连接。 46 | 47 | #### 内连接 48 | 内连接在SQL中是JOIN|INNER JOIN;在Mysql中这两者是等价的。内连接查询返回两个表中在ON后面指定的列条件相同时的行,内连接实际执行流程如下所示: 49 | ```sql 50 | SELECT tbl1.col1, tbl2.col2 51 | FROM tbl1 52 | INNER JOIN tbl2 53 | ON tbl1.col3 = tbl2.col3 54 | WHERE tbl1.col1 IN(5, 6); 55 | 56 | outer_iter = iterator over tbl1 where col1 IN(5, 6) 57 | outer_row = outer_iter.next 58 | while outer_row 59 | inner_iter = iterator over tbl2 where col3 = outer_row.col3 60 | inner_row = inner_iter.next 61 | while inner_row 62 | output(outer_row.col1, inner_row.col2) 63 | inner_row = inner_iter.next 64 | end 65 | outer_row = outer_iter.next 66 | end 67 | ``` 68 | 69 | 我们使用上述创建的数据测试一下,例如: 70 | ```sql 71 | SELECT Persons.id, Persons.LastName, Persons.FirstName, Orders.id, Orders.OrderNo, Orders.Pid 72 | FROM Persons 73 | INNER JOIN Orders 74 | ON Persons.id = Orders.Pid 75 | ``` 76 | 查询的结果如下图所示: 77 | 78 | ![sql-join-result](../img/7-sql-join.jpg) 79 | 80 | 总结:INNER JOIN会返回符合ON条件的所有行,A表中的记录a对应B表中的多个记录会以重复记录a的的方式对应不同的多条B表记录出现在结果集中,同样B表中的一条记录b对应A表中的多条记录时会以多条A表记录对应记录b的方式出现在结果集中。 81 | 82 | #### 外连接 83 | 外连接分为左外连接LEFT JOIN和右外连接RIGHT JOIN。这两者之间是可以转换的,这里我们就以LEFT JOIN为例简单分析一下,其执行流程如下所示: 84 | ```sql 85 | SELECT tbl1.col1, tbl2.col2 86 | FROM tbl1 87 | LEFT OUTER JOIN tbl2 88 | ON tbl1.col3 = tbl2.col3 89 | WHERE tbl1.col1 IN(5, 6); 90 | 91 | outer_iter = iterator over tbl1 where col1 IN(5, 6) 92 | outer_row = outer_iter.next 93 | while outer_row 94 | inner_iter = iterator over tbl2 where col3 = outer_row.col3 95 | inner_row = inner_iter.next 96 | if inner_row { 97 | while inner_row 98 | output(outer_row.col1, inner_row.col2) 99 | inner_row = inner_iter.next 100 | end 101 | } else { 102 | output(outer_row.col1, null) 103 | } 104 | outer_row = outer_iter.next 105 | end 106 | ``` 107 | 从上述的执行流程,我们可以看到和JOIN之间的区别是,如果对于一个左表记录a,即使右表没有匹配的记录,仍然会返回该记录a,其对应的右表字段为NULL。我们使用上述创建的数据测试一下,例如: 108 | ```sql 109 | SELECT Persons.id, Persons.LastName, Persons.FirstName, Orders.id,Orders.OrderNo,Orders.Pid 110 | FROM Persons 111 | LEFT JOIN Orders 112 | ON Persons.id = Orders.Pid 113 | ``` 114 | 查询的结果如下图所示: 115 | 116 | ![sql-left-join](../img/7-sql-left-join.jpg) 117 | 118 | #### 全连接 119 | FULL JOIN全连接会从左表和右表那里返回所有的行。如果左表的记录在右表没有匹配或者右表的记录在左表没有匹配,这些记录仍然会列出,不存在的字段会以NULL补充。可以认为是LEFT JOIN和RIGHT JOIN操作的合集。 120 | ```sql 121 | SELECT Persons.id, Persons.LastName, Persons.FirstName, Orders.id,Orders.OrderNo,Orders.Pid 122 | FROM Persons 123 | RIGHT JOIN Orders 124 | ON Persons.id = Orders.Pid 125 | ``` 126 | 需要注意的是:mysql并不支持全连接,一般利用UNION联合查询代替,也就是左外连接和右外连接的联合查询。 127 | ```sql 128 | SELECT * FROM t1 129 | LEFT JOIN t2 ON t1.id = t2.id 130 | UNION 131 | SELECT * FROM t1 132 | RIGHT JOIN t2 ON t1.id = t2.id 133 | ``` 134 | 仍然是使用上述的数据测试一下,例如: 135 | ```sql 136 | SELECT * FROM Persons 137 | LEFT JOIN Orders ON Persons.id = Orders.Pid 138 | UNION 139 | SELECT * FROM Persons 140 | RIGHT JOIN Orders ON Persons.id = Orders.Pid 141 | ``` 142 | 查询的结果如下所示: 143 | 144 | ![sql-full-join](../img/7-sql-full-join.jpg) 145 | 146 | #### 总结 147 | 对于所有的连接类型而言,就是将满足ON匹配条件的对应记录都合成为一条记录出现在结果集中,对于两个表中的某条记录可能存在:一对多或者多对一的情况会在结果集中形成多条记录,只是该表中查询的字段信息相同而已。 148 | * 对于内连接查询到的是一个符合ON匹配条件的在两个表中都存在记录的结果集 149 | * 对于外连接,以左外连接为例,其结果是一个包含所有左表记录且符合ON匹配条件时的右表记录的结果集,如果左表的某条记录和右表的多条记录匹配,结果集中就存在同一个左表记录对应多个右表记录,如果左表的某条记录没有匹配的右表记录,则对应字段为NULL 150 | * 对于全连接,就是把左表右表都包含在内,如果符合ON条件的匹配在结果集中形成一条记录,如果对左表或者右表中的某一条记录在对方表中不存在ON匹配时,就以单独一条记录出现在结果集中,对方表不存在的信息以NULL 151 | 152 | ### 自连接 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /7/7.6 子查询.md: -------------------------------------------------------------------------------- 1 | ## 7.6 子查询详解 2 | 3 | 在一个表表达式中可以调用另一个表表达式,这个被调用的表表达式叫做子查询(subquery)。子查询的结果传递给调用它的表表达式继续处理。 4 | 5 | ### 分类 6 | 子查询按返回结果集可以分为记录集子查询和标量子查询: 7 | * 记录集子查询一般是一个行的集合或者一个列的集合,这种子查询一般用在父查询的FROM子句或者WHERE子句中 8 | * 标量子查询:返回的结果集是一个标量值,该子查询可以使用在一个标量表达式的任何地方 9 | 10 | 子查询按对返回结果集的调用方法可分为FROM子查询和WHERE子查询: 11 | * FROM子查询:把子查询的结果当作一张临时表供外层再次查询 12 | * WHERE子查询:把子查询的结果当作外层查询的比较条件,这种类型的子查询比较复杂,之后我们重点分析该类型的子查询 13 | 14 | ### where子查询 15 | 按照返回结果集的不同,where比较条件可以分为单行运算符(=, !=, >, >=, <, <=),多行运算符(IN, ANY, ALL)和EXISTS运算符。我们建立三张表用于分析该三类查询: 16 | ```sql 17 | CREATE TABLE Student ( 18 | sid int unsigned NOT NULL AUTO_INCREMENT, 19 | name char(16) NOT NULL DEFAULT '', 20 | age int unsigned NOT NULL, 21 | sex char(1) NOT NULL DEFAULT 'F', 22 | address varchar(10) NOT NULL DEFAULT '' 23 | ); 24 | 25 | CREATE TABLE Course ( 26 | cid int unsigned NOT NULL AUTO_INCREMENT, 27 | name char(16) NOT NULL DEFAULT '', 28 | tid int unsigned NOT NULL 29 | ); 30 | 31 | CREATE TABLE SC ( 32 | sid int unsigned NOT NULL AUTO_INCREMENT, 33 | cid int unsigned NOT NULL, 34 | score int unsigned NOT NULL 35 | ); 36 | ``` 37 | 38 | #### 单行运算符 39 | 单行运算符逻辑一般比较简单,这里简单举一个例子说明一下。获取和10号学生性别年龄相同的学生信息: 40 | ```sql 41 | SELECT * 42 | FROM Student 43 | WHERE (age, sex) = ( 44 | SELECT age, sex 45 | FROM Student 46 | WHERE sid = 10 47 | ); 48 | ``` 49 | 50 | #### 多行运算符 51 | 这里以IN为例说明一下:获取所有选修10号课程,并且成绩大于0的学生信息: 52 | ```sql 53 | SELECT * 54 | FROM Student 55 | WHERE sid IN ( 56 | SELECT sid 57 | FROM SC 58 | WHERE SC.cid = 10 59 | AND SC.score > 0 60 | ); 61 | ``` 62 | 我们发现该子查询实际上可以使用连接解决,如下所示: 63 | ```sql 64 | SELECT Student.* 65 | FROM Student 66 | JOIN SC 67 | ON Student.sid = SC.sid 68 | WHERE SC.score > 0 AND SC.cid = 10; 69 | ``` 70 | 71 | #### EXISTS运算符 72 | EXISTS子查询返回值是true或者false,是把外层查询的结果,拿到内层查询测试,如果满足内层查询,则返回true,然后外层查询将该行记录放到结果集中,否则不放入。举个例子说明: 73 | ```sql 74 | SELECT * 75 | FROM Student 76 | WHERE EXISTS ( 77 | SELECT * 78 | FROM SC 79 | WHERE sid = Student.sid 80 | AND cid = 10 81 | AND score > 0 82 | ); 83 | ``` 84 | 该子查询实际上也是获取所有选修10号课程,并且成绩不为0的学生信息(与上面的IN查询实际上是等价的)。 85 | 86 | 我们简单解释一下该查询的执行过程:首先取出Student表的一行记录,得到其sid列的值(比如1100),然后将该值代入到子查询中,若能找到这样的一条记录,则说明1100的学生选修了10号课程并且成绩大于0,此时EXISTS返回true,则外层查询将Student表的这行记录放到结果集中。以此类推,遍历完Student表中的所有记录后,就能得到所有选修10号课程并且成绩不为0的学生信息。 87 | 88 | 我们发现:EXISTS的结果和IN是一样的,两者之间的区别在于:若子查询表较大则用EXISTS子查询效率更高(内层索引),而子查询表较小则用IN子查询效率更高(外层索引)。 89 | -------------------------------------------------------------------------------- /7/7.7 分库分表.md: -------------------------------------------------------------------------------- 1 | ## 7.7 分库分表 2 | 3 | ### 7.7.1 简介 4 | 简单来说,数据的切分就是通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主机)中,以达到分散单台设备负载的效果,即分库分表。 5 | 6 | 数据的切分根据其切分规则的类型,可以分为如下两种切分模式。 7 | * 垂直切分:把单一的表拆分成多个表,并分散到不同的数据库(主机)上 8 | * 水平切分:根据表中数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库(主机)上 9 | 10 | ### 垂直切分 11 | 一个数据库由多个表构成,每个表对应不同的业务,垂直切分是指按照业务将表进行分类,将其分布到不同的数据库上,这样就将数据分担到了不同的库上(专库专用)。例如电商系统有如下几张表: 12 | ``` 13 | --------------+--------------+------------------ 14 | 用户信息(User)+ 交易记录(Pay)+ 商品(Commodity)| 15 | --------------+--------------+------------------ 16 | ``` 17 | 垂直切分就是根据每个表的不同业务进行切分,比如User表、Pay表和Commodity表,将每个表切分到不同的数据库上。 18 | 19 | 垂直切分的优点如下: 20 | * 拆分后业务清晰,拆分规则明确 21 | * 系统之间进行整合或扩展很容易 22 | * 按照成本、应用的等级、应用的类型等将表放到不同的机器上,便于管理 23 | * 便于实现动静分离、冷热分离的数据库表的设计模式 24 | 25 | 垂直切分的缺点如下: 26 | * 部分业务表无法关联(Join),只能通过接口方式解决,提高了系统的复杂度 27 | * 受每种业务的不同限制,存在单库性能瓶颈,不易进行数据扩展和提升性能 28 | * 事务处理复杂 29 | 30 | 垂直切分除了用于分解单库单表的压力,也用于实现冷热分离,也就是根据数据的活跃度进行拆分,因为对拥有不同活跃度的数据的处理方式不同。例如在微博系统的设计中,一个微博对象包括文章标题、作者、分类、创建时间等属性字段,这些字段的变化频率低,查询次数多,叫作冷数据。而博客的浏览量、回复数、点赞数等类似的统计信息,或者别的变化频率比较高的数据,叫作活跃数据或者热数据。 31 | 32 | 在设计数据库表结构时,就考虑垂直拆分,根据冷热分离、动静分离的原则,再根据使用的存储引擎的特点,对冷数据可以使用MyISAM,能更好地进行数据查询;对热数据可以使用InnoDB,有更快的更新速度,这样能够有效提升性能。 33 | 34 | 其次对读多写少的冷数据可配置更多的从库来化解大量查询请求的压力;对于热数据,可以使用多个主库构建分库分表的结构。 35 | 36 | 最后对于一些特殊的活跃数据或者热点数据,也可以考虑使用Redis之类的缓存,等累计到一定的量后再更新数据库。例如在记录微博点赞数量的业务中,点赞数量被存储在缓存中,每增加1000个点赞,才写一次数据。 37 | 38 | ### 水平切分 39 | 与垂直切分对比,水平切分不是将表进行分类,而是将其按照某个字段的某种规则分散到多个库中,在每个表中包含一部分数据,所有表加起来就是全量的数据。 40 | 41 | 这种切分方式根据单表的数据量的规模来切分,保证单表的容量不会太大,从而保证了单表的查询等处理能力,例如将用户的信息表拆分成User1、User2等,表结构是完全一样的。我们通常根据某些特定的规则来划分表,比如根据用户的ID来取模划分。 42 | 43 | 例如,在博客系统中,当读取博客的量很大时,就应该采取水平切分来减少每个单表的压力,并提升性能。 44 | 45 | 以微博表为例,当同时有100万个用户在浏览时,如果是单表,则单表会进行100万次请求,假如是单库,数据库就会承受100万次的请求压力;假如将其分为100个表,并且分布在10个数据库中,每个表进行1万次请求,则每个数据库会承受10万次的请求压力,虽然这不可能绝对平均,但是可以说明问题,这样压力就减少了很多,并且是成倍减少的。 46 | 47 | 水平切分的优点如下: 48 | * 单库单表的数据保持在一定的量级,有助于性能的提高 49 | * 切分的表的结构相同,应用层改造较少,只需要增加路由规则即可 50 | * 提高了系统的稳定性和负载能力 51 | 52 | 水平切分的缺点如下: 53 | * 切分后,数据是分散的,很难利用数据库的Join操作,跨库Join性能较差 54 | * 拆分规则难以抽象 55 | * 分片事务的一致性难以解决 56 | * 数据扩容的难度和维护量极大 57 | 58 | 综上所述,垂直切分和水平切分的共同点如下: 59 | * 存在分布式事务的问题 60 | * 存在跨节点Join的问题 61 | * 存在跨节点合并排序、分页的问题 62 | * 存在多数据源管理的问题 63 | 64 | 在了解这两种切分方式的特点后,我们就可以根据自己的业务需求来选择,通常会同时使用这两种切分方式,垂直切分更偏向于业务拆分的过程,在技术上我们更关注水平切分的方案,之后我们会主要谈论水平切分方案中的技术和难题。 65 | 66 | ### 路由过程 67 | 我们在设计表时需要确定对表按照什么样的规则进行分库分表。例如,当有新用户时,程序得确定将此用户的信息添加到哪个表中;同理,在登录时我们需要通过用户的账号找到数据库中对应的记录,所有这些都需要按照某一规则进行路由请求,因为请求所需要的数据分布在不同的分片表中。 68 | 69 | 针对输入的请求,通过分库分表规则查找到对应的表和库的过程叫作路由。例如,分库分表的规则是user_id % 4,当用户新注册了一个账号时,假设用户的ID是123,我们就可以通过123 % 4 = 3确定此账号应该被保存在User3表中。当ID为123的用户登录时,我们可通过123 % 4 = 3计算后,确定其被记录在User3中。 70 | 71 | ### 分片维度 72 | 对数据切片有不同的切片维度,可以参考Mycat提供的切片方式,这里只介绍两种最常用的切片维度。 73 | 74 | #### 哈希分片 75 | 对数据的某个字段求哈希,再除以分片总数后取模,取模后相同的数据为一个分片,这样的将数据分成多个分片的方法叫作哈希分片。 76 | 77 | 按照哈希分片常常应用于数据没有时效性的情况,比如所有数据无论是在什么时间产生的,都需要进行处理或者查询,例如支付行业的客户要求可以对至少1年以内的交易进行查询和退款,那么1年以内的所有交易数据都必须停留在交易数据库中,否则就无法查询和退款。 78 | 79 | 如果这家公司在一年内能做10亿条交易,假设每个数据库分片能够容纳5000万条数据,则至少需要20个表才能容纳10亿条交易。在路由时,我们根据交易ID进行哈希取模来找到数据属于哪个分片,因此,在设计系统时要充分考虑如何设计数据库的分库分表的路由规则。 80 | 81 | 这种切片方式的好处是数据切片比较均匀,对数据压力分散的效果较好,缺点是数据分散后,对于查询需求需要进行聚合处理。 82 | 83 | #### 范围分片 84 | 与按照哈希切片不同,这种方式是按照数据的取值范围将数据分布到不同的分片上。 85 | 86 | 例如交易系统中,不同时间的交易数据查询需求不同。比如距离现在1个季度的数据会访问频繁,距离现在两个季度的数据可能不会再有更新需求,距离现在3个季度的数据基本上没有查询需求。针对这种情况,可以通过按照时间进行切片,针对不同的访问频率使用不同档次的硬件资源来节省成本:假设距离现在1个季度的数据访问频率最高,我们就用更好的硬件来运行这个分片;假设距离现在3个季度的数据没有任何访问需求,我们就可以将其整体归档,以方便DBA操作。 87 | 88 | 在实际的生产实践中,按照哈希切片和范围切片都是常用的分库分表方式,并被广泛使用,有时可以结合使用这两种方式。例如:对交易数据先按照季度进行切片,然后对于某一季度的数据按照主键哈希进行切片。 89 | 90 | ### 读写分离 91 | 在实际应用中的绝大多数情况下读操作远大于写操作。MySQL提供了读写分离的机制,所有写操作必须对应到主库,读操作可以在主库和从库机器上进行。主库与从库的结构完全一样,一个主库可以有多个从库,甚至在从库下还可以挂从库,这种一主多从的方式可以有效地提高数据库集群的吞吐量。 92 | 93 | 在DBA领域一般配置主-主-从或者主-从-从两种部署模型。 94 | 95 | 所有写操作都先在主库上进行,然后异步更新到从库上,所以从主库同步到从库机器有一定的延迟,当系统很繁忙时,延迟问题会更加严重,从库机器数量的增加也会使这个问题更严重。 96 | 97 | 此外,主库是集群的瓶颈,当写操作过多时会严重影响主库的稳定性,如果主库挂掉,则整个集群都将不能正常工作。 98 | 99 | 根据以上特点,我们总结一些最佳实践如下: 100 | * 当读操作压力很大时,可以考虑添加从库机器来分解大量读操作带来的压力,但是当从库机器达到一定的数量时,就需要考虑分库来缓解压力了 101 | * 当写压力很大时,就必须进行分库操作了 102 | 103 | 可能会因为种种原因,集群中的数据库硬件配置等会不一样,某些性能高,某些性能低,这时可以通过程序控制每台机器读写的比重来达到负载均衡,这需要更加复杂的读写分离的路由规则。 104 | 105 | ### 分库分表引起的问题 106 | 分库分表按照某种规则将数据的集合拆分成多个子集合,数据的完整性被打破,因此在某种场景下会产生多种问题。 107 | 108 | #### 扩容与迁移 109 | 在分库分表后,如果涉及的分片已经达到了承载数据的最大值,就需要对集群进行扩容。扩容是很麻烦的,一般会成倍地扩容。通用的扩容方法包括如下5个步骤: 110 | * 按照新旧分片规则,对新旧数据库进行双写。 111 | * 将双写前按照旧分片规则写入的历史数据,根据新分片规则迁移写入新的数据库。 112 | * 将按照旧的分片规则查询改为按照新的分片规则查询。 113 | * 将双写数据库逻辑从代码中下线,只按照新的分片规则写入数据。 114 | * 删除按照旧分片规则写入的历史数据。 115 | 116 | 这里,在第2步迁移历史数据时,由于数据量很大,通常会导致不一致,因此,先清洗旧的数据,洗完后再迁移到新规则的新数据库下,再做全量对比,对比后评估在迁移的过程中是否有数据的更新,如果有的话就再清洗、迁移,最后以对比没有差距为准。 117 | 118 | 如果是历史交易数据,则最好将动静数据分离,随着时间的流逝,某个时间点之前的数据是不会被更新的,我们就可以拉长双写的时间窗口,这样在足够长的时间流逝后,只需迁移那些不再被更新的历史数据即可,就不会在迁移的过程中由于历史数据被更新而导致代理不一致。 119 | 120 | 在数据量巨大时,如果数据迁移后没法进行全量对比,就需要进行抽样对比,在进行抽样对比时要根据业务的特点选取一些具有某类特征性的数据进行对比。 121 | 122 | 在迁移的过程中,数据的更新会导致不一致,可以在线上记录迁移过程中的更新操作的日志,迁移后根据更新日志与历史数据共同决定数据的最新状态,来达到迁移数据的最终一致性。 123 | 124 | #### 多维度查询 125 | 在分库分表以后,如果查询的标准是分片的主键,则可以通过分片规则路由并查询;但是对于其他键的查询、范围查询、关联查询、查询结果排序等,并不是按照分库分表维度来查询的。 126 | 127 | 例如,用户购买了商品,需要将交易记录保存下来,那么如果按照买家的纬度分表,则每个买家的交易记录都被保存在同一表中,我们可以很快、很方便地查到某个买家的购买情况,但是某个商品被购买的交易数据很有可能分布在多张表中,查找起来比较麻烦。反之,按照商品维度分表,则可以很方便地查找到该商品的购买情况,但若要查找到买家的交易记录,则会比较麻烦。常见的解决方式如下: 128 | * 在多个分片表查询后合并数据集,这种方式的效率很低 129 | * 记录两份数据,一份按照买家纬度分表,一份按照商品维度分表 130 | * 通过搜索引擎解决,但如果实时性要求很高,就需要实现实时搜索 131 | 132 | 实际上,在高并发的服务平台下,交易系统是专门做交易的,因为交易是核心服务,SLA的级别比较高,所以需要和查询系统分离,查询一般通过其他系统进行,数据也可能是冗余存储的。 133 | 134 | 这里再举个例子,在某电商交易平台下,可能有买家查询自己在某一时间段的订单,也可能有卖家查询自己在某一时间段的订单,如果使用了分库分表方案,则这两个需求是难以同时满足。因此,通用的解决方案是,在交易产生时生成一份按照买家分片的数据副本和一份按照卖家分片的数据副本,则查询时可以满足之前的两个需求,因此,查询的数据和交易的数据可能是分别存储的,并从不同的系统提供接口。 135 | 136 | 另外,在电商系统中,在一个交易订单生成后,一般需要引用到订单中交易的商品实体,如果简单地引用,若商品的金额等信息发生变化,则会导致原订单上的商品信息也会发生变化,这样买家会很疑惑。因此,通用的解决方案是在交易系统中存储商品的快照,在查询交易时使用交易的快照,因为快照是个静态数据,都不会更新,则解决了这个问题。可见查询的问题最好在单独的系统中使用其他技术来解决,而不是在交易系统中实现各类查询功能;当然,也可以通过对商品的变更实施版本化,在交易订单中引用商品的版本信息,在版本更新时保留商品的旧版本,这也是一种不错的解决方案。 137 | 138 | 最后,关联的表有可能不在同一数据库中,所以基本不可能进行联合查询,需要借助大数据技术来实现,也就是上面所说的第3种方法,即通过大数据技术统一聚合和处理关系型数据库的数据,然后对外提供查询操作。 139 | 140 | 通过大数据方式来提供聚合查询的方式如下图所示: 141 | 142 | ![mysql-shard-se](../img/7-mysql-shard-se.png) 143 | 144 | #### 同组数据跨库问题 145 | 要尽量把同一组数据放到同一台数据库服务器上,不但在某些场景下可以利用本地事务的强一致性,还可以使这组数据自治。以电商为例,我们的应用有两个数据库db0和db1,分库分表后,按照id维度,将卖家A的交易信息存放到db0中。当数据库db1挂掉时,卖家A的交易信息不受影响,依然可以正常使用。也就是说,要避免数据库中的数据依赖另一数据库中的数据。 146 | 147 | #### 分布式事务 148 | 分布式事务是一个很复杂的话题,之后我们会详细讨论这个话题,这里不再详述。 -------------------------------------------------------------------------------- /7/7.8 大表优化.md: -------------------------------------------------------------------------------- 1 | ## 7.8 大表优化 2 | 3 | 当MySQL单表记录数过大时,增删改查性能都会急剧下降,可以参考以下步骤来优化: 4 | 5 | ### 单表优化 6 | 除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑、部署、运维的各种复杂度,一般以整型值为主的表在千万级以下,字符串为主的表在五百万以下是没有太大问题的。我们看一下单表优化方案: 7 | 8 | #### 字段 9 | * 尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED 10 | * VARCHAR的长度只分配真正需要的空间 11 | * 避免使用NULL字段,很难查询优化且占用额外索引空间 12 | * 使用枚举或整数代替字符串类型 13 | * 尽量使用TIMESTAMP而非DATETIME 14 | * 单表不要有太多字段,建议在20以内 15 | 16 | #### 索引 17 | 索引并不是越多越好,要根据查询有针对性的创建,一般考虑在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描。 18 | 19 | 应尽量避免在WHERE子句中对字段进行NULL值判断,否则查询引擎放弃使用索引而进行全表扫描 20 | 选择性很差的字段不适合建索引,例如“性别”这种只有两个值的字段 21 | 字符字段只建前缀索引 22 | 字符字段最好不要做主键 23 | 不用外键,由程序保证约束 24 | 尽量不用UNIQUE,由程序保证约束 25 | 使用多列索引时注意索引顺序和查询条件保持一致,同时删除不必要的单列索引 26 | 27 | #### 查询SQL 28 | 可通过开启慢查询日志来找出较慢的SQL 29 | 不在查询语句中做列运算:`SELECT id WHERE age + 1 = 10`,任何对列的操作都将导致全表扫描,它包括数据库教程函数、计算表达式,查询时要尽可能将操作移至等号右边 30 | sql语句尽可能简单:一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库 31 | 不要使用SELECT * 32 | 不要使用OR或者IN 33 | 避免%xxx前缀模糊式查询 34 | 少用JOIN 35 | 使用同类型进行比较,比如用'123'和'123'比,123和123比 36 | 尽量避免在WHERE子句中使用!=或<>操作符,否则查询引擎放弃使用索引而进行全表扫描 37 | 列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大 38 | 39 | #### 系统调优参数 40 | 可以使用下面几个工具来做基准测试: 41 | * sysbench:一个模块化,跨平台以及多线程的性能测试工具 42 | * iibench-mysql:基于Java的MySQL/Percona/MariaDB索引进行插入性能测试工具 43 | * tpcc-mysql:Percona开发的TPC-C测试工具 44 | 45 | 具体的调优参数内容较多,具体可参考官方文档,这里介绍一些比较重要的参数: 46 | * back_log:back_log值指出在MySQL暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。也就是说如果MySql的连接数据达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源。可以从默认的50升至500 47 | * wait_timeout:数据库连接闲置时间,闲置连接会占用内存资源。可以从默认的8小时减到半小时 48 | * max_user_connection: 最大连接数,默认为0无上限,最好设一个合理上限 49 | * thread_concurrency:并发线程数,设为CPU核数的两倍 50 | * skip_name_resolve:禁止对外部连接进行DNS解析,消除DNS解析时间,但需要所有远程主机用IP访问 51 | * key_buffer_size:索引块的缓存大小,增加会提升索引处理速度,对MyISAM表性能影响最大。对于内存4G左右,可设为256M或384M,通过查询show status like 'key_read%',保证key_reads / key_read_requests在0.1%以下最好 52 | * innodb_buffer_pool_size:缓存数据块和索引块,对InnoDB表性能影响最大。通过查询show status like 'Innodb_buffer_pool_read%',保证 (Innodb_buffer_pool_read_requests – 53 | Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests越高越好 54 | * innodb_additional_mem_pool_size:InnoDB存储引擎用来存放数据字典信息以及一些内部数据结构的内存空间大小,当数据库对象非常多的时候,适当调整该参数的大小以确保所有数据都能存放在内存中提高访问效率,当过小的时候,MySQL会记录Warning信息到数据库的错误日志中,这时就需要该调整这个参数大小 55 | * innodb_log_buffer_size:InnoDB存储引擎的事务日志所使用的缓冲区,一般来说不建议超过32MB 56 | * query_cache_size:缓存MySQL中的ResultSet,也就是一条SQL语句执行的结果集,所以仅仅只能针对select语句。当某个表的数据有任何任何变化,都会导致所有引用了该表的select语句在Query 57 | Cache中的缓存数据失效。所以,当我们的数据变化非常频繁的情况下,使用Query Cache可能会得不偿失。根据命中率(Qcache_hits/(Qcache_hits+Qcache_inserts)*100))进行调整,一般不建议太大,256MB可能已经差不多了,大型的配置型静态数据可适当调大。可以通过命令show status like 'Qcache_%'查看目前系统Query catch使用大小 58 | * read_buffer_size:MySql读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySql会为它分配一段内存缓冲区。如果对表的顺序扫描请求非常频繁,可以通过增加该变量值以及内存缓冲区大小提高其性能 59 | * sort_buffer_size:MySql执行排序使用的缓冲大小。如果想要增加ORDER BY的速度,首先看是否可以让MySQL使用索引而不是额外的排序阶段。如果不能,可以尝试增加sort_buffer_size变量的大小 60 | * read_rnd_buffer_size:MySql的随机读缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySql会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySql会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。 61 | * record_buffer:每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,可能想要增加该值 62 | * thread_cache_size:保存当前没有与连接关联但是准备为后面新的连接服务的线程,可以快速响应连接的线程请求而无需创建新的 63 | * table_cache:类似于thread_cache_size,但用来缓存表文件,对InnoDB效果不大,主要用于MyISAM 64 | 65 | ### 读写分离 66 | 也是目前常用的优化,从库读主库写,一般不要采用双主或多主引入很多复杂性。同时目前很多拆分的解决方案同时也兼顾考虑了读写分离。 67 | 68 | ### 缓存 69 | 缓存可以发生在这些层次,从前到后: 70 | * 浏览器客户端:用户端的缓存 71 | * Web层:针对web页面做缓存 72 | * 应用服务层:这里可以通过编程手段对缓存做到更精准的控制和更多的实现策略,这里缓存的对象是数据传输对象Data Transfer Object 73 | * 数据访问层:比如MyBatis针对SQL语句做缓存,而Hibernate可以精确到单个记录,这里缓存的对象主要是持久化对象Persistence Object 74 | * MySQL内部:在系统调优参数介绍了相关设置 75 | 76 | 可以根据实际情况在一个层次或多个层次结合加入缓存。这里重点介绍下服务层的缓存实现,目前主要有两种方式: 77 | * 直写式:在数据写入数据库后,同时更新缓存,维持数据库与缓存的一致性。这也是当前大多数应用缓存框架如Spring Cache的工作方式。这种实现非常简单,同步好,但效率一般。 78 | * 回写式:当有数据要写入数据库时,只会更新缓存,然后异步批量的将缓存数据同步到数据库上。这种实现比较复杂,需要较多的应用逻辑,同时可能会产生数据库与缓存的不同步,但效率非常高。 79 | 80 | ### 表分区 81 | MySQL在5.1版引入的分区是一种简单的水平拆分,用户需要在建表的时候加上分区参数,对应用是透明的无需修改代码。对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成,实现分区的代码实际上是通过对一组底层表的对象封装,但对SQL层来说是一个完全封装底层的黑盒子。MySQL实现分区的方式也意味着索引也是按照分区的子表定义,没有全局索引。 82 | 83 | ![mysql-partition](../img/7-mysql-partition.png) 84 | 85 | 用户的SQL语句是需要针对分区表做优化,SQL条件中要带上分区条件的列,从而使查询定位到少量的分区上,否则就会扫描全部分区,可以通过EXPLAIN PARTITIONS来查看某条SQL语句会落在那些分区上,从而进行SQL优化,如下图5条记录落在两个分区上: 86 | ```shell 87 | mysql> explain partitions select count(1) from user_partition where id in (1,2,3,4,5); 88 | +----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+ 89 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra | 90 | +----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+ 91 | | 1 | SIMPLE | user_partition | p1,p4 | range | PRIMARY | PRIMARY | 8 | NULL | 5 | Using where; Using index | 92 | +----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+ 93 | 1 row in set (0.00 sec) 94 | ``` 95 | 96 | #### 分区的好处 97 | * 可以让单表存储更多的数据 98 | * 分区表的数据更容易维护,可以通过清楚整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作 99 | * 部分查询能够从查询条件确定只落在少数分区上,速度会很快 100 | * 分区表的数据还可以分布在不同的物理设备上,从而搞笑利用多个硬件设备 101 | * 可以使用分区表赖避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争 102 | * 可以备份和恢复单个分区 103 | 104 | #### 分区的限制和缺点 105 | * 一个表最多只能有1024个分区 106 | * 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来 107 | * 分区表无法使用外键约束 108 | * NULL值会使分区过滤无效 109 | * 所有分区必须使用相同的存储引擎 110 | 111 | #### 分区的类型 112 | * RANGE分区:基于属于一个给定连续区间的列值,把多行分配给分区 113 | * LIST分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择 114 | * HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式 115 | * KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值 116 | 117 | #### 分区适合的场景 118 | 最适合的场景数据的时间序列性比较强,则可以按时间来分区,如下所示: 119 | ```sql 120 | CREATE TABLE members ( 121 | firstname VARCHAR(25) NOT NULL, 122 | lastname VARCHAR(25) NOT NULL, 123 | username VARCHAR(16) NOT NULL, 124 | email VARCHAR(35), 125 | joined DATE NOT NULL 126 | ) 127 | PARTITION BY RANGE( YEAR(joined) ) ( 128 | PARTITION p0 VALUES LESS THAN (1960), 129 | PARTITION p1 VALUES LESS THAN (1970), 130 | PARTITION p2 VALUES LESS THAN (1980), 131 | PARTITION p3 VALUES LESS THAN (1990), 132 | PARTITION p4 VALUES LESS THAN MAXVALUE 133 | ); 134 | ``` 135 | 查询时加上时间范围条件效率会非常高,同时对于不需要的历史数据能很容易的批量删除。 136 | 137 | 如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存 138 | 139 | ### 垂直拆分 140 | 垂直分库是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联。 141 | 142 | 比如原始的用户表是: 143 | 144 | ![user](../img/7-user-full.png) 145 | 146 | 垂直拆分后是: 147 | 148 | ![user](../img/7-user.png) 149 | 150 | ![user-extra](../img/7-user-extra.png) 151 | 152 | 垂直拆分的优点是: 153 | * 可以使得行数据变小,一个数据块就能存放更多的数据,在查询时就会减少I/O次数 154 | * 可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起 155 | * 数据维护简单 156 | 157 | 缺点是: 158 | * 主键出现冗余,需要管理冗余列 159 | * 会引起表连接JOIN操作,可以通过在业务服务器上进行join来减少数据库压力 160 | * 依然存在单表数据量过大的问题(需要水平拆分) 161 | * 事务处理复杂 162 | 163 | ### 水平拆分 164 | 水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的MySQL表或库,达到分布式的效果,能够支持非常大的数据量。前面的表分区本质上也是一种特殊的库内分表。库内分表,仅仅是单纯的解决了单一表数据过大的问题,由于没有把表的数据分布到不同的机器上,因此对于减轻MySQL服务器的压力来说,并没有太大的作用,大家还是竞争同一个物理机上的IO、CPU资源等,这个就要通过分库来解决。 165 | 166 | 前面垂直拆分的用户表如果进行水平拆分,结果是: 167 | 168 | ![user-split-horizon](../img/7-user-split-horizon.png) 169 | 170 | 实际情况中往往会是垂直拆分和水平拆分的结合,即将Users_A_M和Users_N_Z再拆成Users和UserExtras,这样一共四张表。 171 | 172 | 水平拆分的优点是: 173 | * 不存在单库大数据和高并发的性能瓶颈 174 | * 提高了系统的稳定性和负载能力 175 | 176 | 缺点是: 177 | * 分片事务一致性难以解决 178 | * 跨节点Join性能差,逻辑复杂 179 | * 数据多次扩展难度跟维护量极大 180 | 181 | 水平拆分原则: 182 | * 能不分就不分,参考单表优化 183 | * 分片数量尽量少,分片尽量均匀分布在多个数据结点上,因为一个查询SQL跨分片越多,则总体性能越差。只在必要的时候进行扩容,增加分片数量 184 | * 分片规则需要慎重选择做好提前规划,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题。一般分片策略为范围分片,枚举分片,一致性Hash分片,这几种分片都有利于扩容 185 | * 尽量不要在一个事务中的SQL跨越多个分片,分布式事务不是一个容易处理的问题 186 | * 查询条件尽量优化,尽量避免`select *`的查询方式,大量数据结果集下,会消耗大量带宽和CPU资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。 187 | * 通过数据冗余和表分区来降低跨库Join的可能 188 | 189 | 这里特别强调一下分片规则的选择问题,如果某个表的数据有明显的时间特征,比如订单、交易记录等,则他们通常比较适合用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤。比较好的方案是,当前活跃的数据,采用跨度比较短的时间段进行分片,而历史性的数据,则采用比较长的跨度存储。 190 | 191 | 总体上来说,分片的选择是取决于最频繁的查询SQL的条件,因为不带任何Where语句的查询SQL,会遍历所有的分片,性能相对最差,因此这种SQL越多,对系统的影响越大,所以我们要尽量避免这种SQL的产生。 192 | 193 | #### 解决方案 194 | 由于水平拆分牵涉的逻辑比较复杂,当前也有了不少比较成熟的解决方案。这些方案分为两大类:客户端架构和代理架构。 195 | 196 | ##### 客户端架构 197 | 通过修改数据访问层,如JDBC、Data Source、MyBatis,通过配置来管理多个数据源,直连数据库,并在模块内完成数据的分片整合,一般以Jar包的方式呈现。这是一个客户端架构的例子: 198 | 199 | ![mysql-shard-client](../img/7-mysql-shard-client.png) 200 | 201 | 可以看到分片的实现是和应用服务器在一起的,通过修改Spring JDBC层来实现。 202 | 203 | 客户端架构的优点是: 204 | * 应用直连数据库,降低外围系统依赖所带来的宕机风险 205 | * 集成成本低,无需额外运维的组件 206 | 207 | 缺点是: 208 | * 限于只能在数据库访问层上做文章,扩展性一般,对于比较复杂的系统可能会力不从心 209 | * 将分片逻辑的压力放在应用服务器上,造成额外风险 210 | 211 | ##### 代理架构 212 | 通过独立的中间件来统一管理所有数据源和数据分片整合,后端数据库集群对前端应用程序透明,需要独立部署和运维代理组件。这是一个代理架构的例子: 213 | 214 | ![mysql-shard-proxy](../img/7-mysql-shard-proxy.png) 215 | 216 | 代理组件为了分流和防止单点,一般以集群形式存在,同时可能需要Zookeeper之类的服务组件来管理。 217 | 218 | 代理架构的优点是: 219 | * 能够处理非常复杂的需求,不受数据库访问层原来实现的限制,扩展性强 220 | * 对于应用服务器透明且没有增加任何额外负载 221 | 222 | 缺点是: 223 | * 需部署和运维独立的代理中间件,成本高 224 | * 应用需经过代理来连接数据库,网络上多了一跳,性能有损失且有额外风险 225 | 226 | ### NoSQL 227 | 在MySQL上做Sharding是一种戴着镣铐的跳舞,事实上很多大表本身对MySQL这种RDBMS的需求并不大,并不要求ACID,可以考虑将这些表迁移到NoSQL,彻底解决水平扩展问题,例如: 228 | * 日志类、监控类、统计类数据 229 | * 非结构化或弱结构化数据 230 | * 对事务要求不强,且无太多关联操作的数据 231 | 232 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | -------------------------------------------------------------------------------- /img/2-10-handle-http-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-10-handle-http-request.png -------------------------------------------------------------------------------- /img/2-3-ngx-module-conf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-3-ngx-module-conf.png -------------------------------------------------------------------------------- /img/2-4-conf-scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-4-conf-scopes.png -------------------------------------------------------------------------------- /img/2-4-ngx-conf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-4-ngx-conf.png -------------------------------------------------------------------------------- /img/2-7-ngx-event-conf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-7-ngx-event-conf.jpg -------------------------------------------------------------------------------- /img/2-9-http-loc-conf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-9-http-loc-conf.jpg -------------------------------------------------------------------------------- /img/2-9-http-main-conf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-9-http-main-conf.jpg -------------------------------------------------------------------------------- /img/2-9-http-merge-conf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-9-http-merge-conf.jpg -------------------------------------------------------------------------------- /img/2-9-http-srv-conf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/2-9-http-srv-conf.jpg -------------------------------------------------------------------------------- /img/3-3-server-start.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-3-server-start.jpg -------------------------------------------------------------------------------- /img/3-4-follower-processors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-4-follower-processors.png -------------------------------------------------------------------------------- /img/3-4-leader-processors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-4-leader-processors.png -------------------------------------------------------------------------------- /img/3-4-zk-leader-election.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-4-zk-leader-election.png -------------------------------------------------------------------------------- /img/3-5-zk-session-activate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-5-zk-session-activate.png -------------------------------------------------------------------------------- /img/3-5-zk-session-bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-5-zk-session-bucket.png -------------------------------------------------------------------------------- /img/3-5-zk-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-5-zk-session.png -------------------------------------------------------------------------------- /img/3-7-create-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-7-create-session.png -------------------------------------------------------------------------------- /img/3-7-proposal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/3-7-proposal.png -------------------------------------------------------------------------------- /img/4-1-kafka-framework.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/4-1-kafka-framework.jpg -------------------------------------------------------------------------------- /img/4-2-offset-manager.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/4-2-offset-manager.jpg -------------------------------------------------------------------------------- /img/4-3-kafka-zk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/4-3-kafka-zk.png -------------------------------------------------------------------------------- /img/7-btree-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-btree-index.png -------------------------------------------------------------------------------- /img/7-btree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-btree.jpg -------------------------------------------------------------------------------- /img/7-index-principle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-index-principle.png -------------------------------------------------------------------------------- /img/7-innodb-mvcc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-innodb-mvcc.png -------------------------------------------------------------------------------- /img/7-innodb-pkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-innodb-pkey.png -------------------------------------------------------------------------------- /img/7-innodb-skey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-innodb-skey.png -------------------------------------------------------------------------------- /img/7-myisam-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-myisam-index.png -------------------------------------------------------------------------------- /img/7-mysql-partition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-mysql-partition.png -------------------------------------------------------------------------------- /img/7-mysql-shard-client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-mysql-shard-client.png -------------------------------------------------------------------------------- /img/7-mysql-shard-proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-mysql-shard-proxy.png -------------------------------------------------------------------------------- /img/7-mysql-shard-se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-mysql-shard-se.png -------------------------------------------------------------------------------- /img/7-sql-full-join.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-sql-full-join.jpg -------------------------------------------------------------------------------- /img/7-sql-isolation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-sql-isolation.jpg -------------------------------------------------------------------------------- /img/7-sql-join-summary.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-sql-join-summary.jpg -------------------------------------------------------------------------------- /img/7-sql-join.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-sql-join.jpg -------------------------------------------------------------------------------- /img/7-sql-left-join.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-sql-left-join.jpg -------------------------------------------------------------------------------- /img/7-sql-order.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-sql-order.jpg -------------------------------------------------------------------------------- /img/7-sql-person.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-sql-person.jpg -------------------------------------------------------------------------------- /img/7-unified-index.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-unified-index.webp -------------------------------------------------------------------------------- /img/7-user-extra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-user-extra.png -------------------------------------------------------------------------------- /img/7-user-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-user-full.png -------------------------------------------------------------------------------- /img/7-user-split-horizon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-user-split-horizon.png -------------------------------------------------------------------------------- /img/7-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianhuih/backend/07f5bc46b1ce8e44479ec70229b8e74eeac20d81/img/7-user.png --------------------------------------------------------------------------------