├── README.md ├── aeron ├── CNC.png ├── 单播交互流程.png ├── 组播交互流程.png ├── 单机数据流示意图.png ├── 双机数据流示意图.png ├── archive双机部署.png ├── architecture.png ├── scenario 1-3.png ├── scenario 1-5.png ├── scenario 2-3.png ├── scenario 2-4.png ├── scenario 3-3.png ├── scenario 3-6.png ├── scenario 4-3.png ├── scenario 4-6.png ├── Circular Buffer.jpg ├── archive原理-同机部署.png ├── publication log buffer创建.png ├── publication log buffer结构.png └── Aeron浅析.md ├── amd ├── 2-1-1.png ├── 2-2-1.png ├── 3-2-1.png ├── 4-1-1.png ├── 4-1-2.png ├── 4-1-3.png ├── 4-1-4.png ├── 4-1-5.png ├── 5-1-1.png ├── 8-1-1.png ├── 8-2-1.png ├── 8-2-2.png ├── 8-3-1.png ├── 8-4-1.png └── amd introduction.md ├── BGN标识体系 ├── 01.png ├── 02.png └── BGN标识体系.md ├── 分布式计算平台BGN介绍.pdf ├── RFS设计与实现 ├── rfs-2-1.png ├── rfs-2-2.png ├── rfs-2-3.png ├── rfs-4-1.png ├── rfs-4-2.png ├── rfs-5-0.png ├── rfs-5-1-1.png ├── rfs-5-1-2.png ├── rfs-5-1-3.png ├── rfs-5-1-4.png ├── rfs-5-1-5.png ├── rfs-5-1-6.png ├── rfs-5-1-7.png └── RFS技术原理与实现剖析.md ├── XCACHE和XFS间通信 ├── 01.png ├── 02.png └── XCACHE和XFS间通信.md ├── XFS设计与实现 ├── xfs-2-1.png ├── xfs-6-1-1.png ├── xfs-6-3-1.png ├── xfs-6-4-1.png ├── xfs-6-4-2.png ├── xfs-6-4-3.png ├── xfs-6-4-4.png ├── xfs-6-4-5.png ├── xfs-6-4-6.png ├── xfs-6-4-7.png ├── xfs-6-4-8.png ├── xfs-6-4-9.png ├── xfs-6-2-1-1.png ├── xfs-6-2-2-1.png ├── xfs-6-5-1-1.png ├── xfs-6-5-1-2.png ├── xfs-6-5-1-3.png ├── xfs-6-5-2-1.png └── XFS介绍.md ├── seastar ├── NUMA_01.png ├── NUMA_02.png ├── concurrence_parallel.jpg ├── seastar DEMO - 神奇的return.md └── seastar DEMO - 网络连接处理.md ├── NGX与BGN融合 ├── 01-component.png ├── 04-data-flow.png ├── 03-orig-merge.png ├── 02-community-response.png └── NGX与BGN融合.md ├── xcache上传模块cupload.md ├── xcache下载模块cdownload.md ├── XFS使用 ├── XFS配置举例.md ├── 依赖包安装.md └── XFS上手.md ├── 面向对象C编程 └── 面向对象C编程.md ├── 关于存储 └── 关于存储.md ├── xcache粘合ngx upstream.md └── 协程基础.md /README.md: -------------------------------------------------------------------------------- 1 | # Knowledge-Sharing 2 | studying and sharing 3 | -------------------------------------------------------------------------------- /aeron/CNC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/CNC.png -------------------------------------------------------------------------------- /amd/2-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/2-1-1.png -------------------------------------------------------------------------------- /amd/2-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/2-2-1.png -------------------------------------------------------------------------------- /amd/3-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/3-2-1.png -------------------------------------------------------------------------------- /amd/4-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/4-1-1.png -------------------------------------------------------------------------------- /amd/4-1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/4-1-2.png -------------------------------------------------------------------------------- /amd/4-1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/4-1-3.png -------------------------------------------------------------------------------- /amd/4-1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/4-1-4.png -------------------------------------------------------------------------------- /amd/4-1-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/4-1-5.png -------------------------------------------------------------------------------- /amd/5-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/5-1-1.png -------------------------------------------------------------------------------- /amd/8-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/8-1-1.png -------------------------------------------------------------------------------- /amd/8-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/8-2-1.png -------------------------------------------------------------------------------- /amd/8-2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/8-2-2.png -------------------------------------------------------------------------------- /amd/8-3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/8-3-1.png -------------------------------------------------------------------------------- /amd/8-4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/amd/8-4-1.png -------------------------------------------------------------------------------- /BGN标识体系/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/BGN标识体系/01.png -------------------------------------------------------------------------------- /BGN标识体系/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/BGN标识体系/02.png -------------------------------------------------------------------------------- /aeron/单播交互流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/单播交互流程.png -------------------------------------------------------------------------------- /aeron/组播交互流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/组播交互流程.png -------------------------------------------------------------------------------- /分布式计算平台BGN介绍.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/分布式计算平台BGN介绍.pdf -------------------------------------------------------------------------------- /aeron/单机数据流示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/单机数据流示意图.png -------------------------------------------------------------------------------- /aeron/双机数据流示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/双机数据流示意图.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-2-1.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-2-2.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-2-3.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-4-1.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-4-2.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-5-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-5-0.png -------------------------------------------------------------------------------- /XCACHE和XFS间通信/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XCACHE和XFS间通信/01.png -------------------------------------------------------------------------------- /XCACHE和XFS间通信/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XCACHE和XFS间通信/02.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-2-1.png -------------------------------------------------------------------------------- /aeron/archive双机部署.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/archive双机部署.png -------------------------------------------------------------------------------- /seastar/NUMA_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/seastar/NUMA_01.png -------------------------------------------------------------------------------- /seastar/NUMA_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/seastar/NUMA_02.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-5-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-5-1-1.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-5-1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-5-1-2.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-5-1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-5-1-3.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-5-1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-5-1-4.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-5-1-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-5-1-5.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-5-1-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-5-1-6.png -------------------------------------------------------------------------------- /RFS设计与实现/rfs-5-1-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/RFS设计与实现/rfs-5-1-7.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-1-1.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-3-1.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-1.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-2.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-3.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-4.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-5.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-6.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-7.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-8.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-4-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-4-9.png -------------------------------------------------------------------------------- /aeron/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/architecture.png -------------------------------------------------------------------------------- /aeron/scenario 1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/scenario 1-3.png -------------------------------------------------------------------------------- /aeron/scenario 1-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/scenario 1-5.png -------------------------------------------------------------------------------- /aeron/scenario 2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/scenario 2-3.png -------------------------------------------------------------------------------- /aeron/scenario 2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/scenario 2-4.png -------------------------------------------------------------------------------- /aeron/scenario 3-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/scenario 3-3.png -------------------------------------------------------------------------------- /aeron/scenario 3-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/scenario 3-6.png -------------------------------------------------------------------------------- /aeron/scenario 4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/scenario 4-3.png -------------------------------------------------------------------------------- /aeron/scenario 4-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/scenario 4-6.png -------------------------------------------------------------------------------- /NGX与BGN融合/01-component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/NGX与BGN融合/01-component.png -------------------------------------------------------------------------------- /NGX与BGN融合/04-data-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/NGX与BGN融合/04-data-flow.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-2-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-2-1-1.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-2-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-2-2-1.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-5-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-5-1-1.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-5-1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-5-1-2.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-5-1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-5-1-3.png -------------------------------------------------------------------------------- /XFS设计与实现/xfs-6-5-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/XFS设计与实现/xfs-6-5-2-1.png -------------------------------------------------------------------------------- /aeron/Circular Buffer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/Circular Buffer.jpg -------------------------------------------------------------------------------- /aeron/archive原理-同机部署.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/archive原理-同机部署.png -------------------------------------------------------------------------------- /NGX与BGN融合/03-orig-merge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/NGX与BGN融合/03-orig-merge.png -------------------------------------------------------------------------------- /seastar/concurrence_parallel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/seastar/concurrence_parallel.jpg -------------------------------------------------------------------------------- /NGX与BGN融合/02-community-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/NGX与BGN融合/02-community-response.png -------------------------------------------------------------------------------- /aeron/publication log buffer创建.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/publication log buffer创建.png -------------------------------------------------------------------------------- /aeron/publication log buffer结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaoyongzhou/Knowledge-Sharing/HEAD/aeron/publication log buffer结构.png -------------------------------------------------------------------------------- /xcache上传模块cupload.md: -------------------------------------------------------------------------------- 1 | # xcache上传模块cupload 2 | 3 | ## 1、背景 4 | 5 | xcache的上传模块cupload支持断点续传,文件大小不限,支持http协议传输。 6 | 7 | 客户端上传工具以分片形式上传文件,分片文件名格式为:<目标文件名>.part_<起始偏移量>_<结束偏移量>_<原始文件长度>,分片文件的内容为原始文件[起始偏移量,结束偏移量)的范围,即左闭右开区间的内容。 8 | 9 | 比如,上传本地文件aa.log到服务器端的ab.log,分片范围为[100, 200),原始文件长度为1000字节,那么分片文件名为ab.log.part_100_200_1000。 10 | 11 | ## 2、服务器端访问 12 | 13 | ### 2.1、按片上传文件 14 | 15 | 举例: 16 | 17 | curl -X POST -sv http://www.upload.com/upload/tmp/ab.log -x 127.1:80 -d "hello world" -H "Content-Range: bytes 0-10/11" 18 | 19 | 创建服务器端文件/tmp/ab.log,内容为“hello world”。 20 | 21 | ### 2.2、按片覆盖文件 22 | 23 | 举例: 24 | 25 | curl -X POST -sv http://www.upload.com/override/tmp/ab.log -x 127.1:80 -d "HE" -H "Content-Range: bytes 0-1/11" 26 | 27 | 修改服务器端文件/tmp/ab.log的[0, 1]区间的内容为“HE”。 28 | 29 | ### 2.3、清空文件 30 | 31 | 举例: 32 | 33 | curl -X GET -sv http://www.upload.com/empty/tmp/ab.log -x 127.1:80 34 | 35 | 置服务器端文件/tmp/ab.log为空文件。 36 | 37 | ### 2.4、按片合并 38 | 39 | 举例: 40 | 41 | curl -X GET -sv http://www.upload.com/merge/tmp/ab.log -x 127.1:80 -H "Content-Range: bytes 0-1/11" 42 | 43 | 将服务器端分片文件/tmp/ab.log.part_0_1_11的内容,追加合并到服务器端文件/tmp/ab.log,然后删除分片文件。 44 | 45 | ### 2.5、删除文件 46 | 47 | 举例: 48 | 49 | curl -X GET -sv http://www.upload.com/delete/tmp/ab.log -x 127.1:80 50 | 51 | 删除服务器端文件/tmp/ab.log。 52 | 53 | ### 2.6、获取文件大小 54 | 55 | 举例: 56 | 57 | curl -X GET -sv http://www.upload.com/size/tmp/ab.log -x 127.1:80 58 | 59 | 获取服务器端文件/tmp/ab.log的大小,单位:字节,由响应头X-File-Size带回。 60 | 61 | ### 2.7、按片计算MD5 62 | 63 | 举例: 64 | 65 | curl -X GET -sv http://www.upload.com/md5/tmp/ab.log -x 127.1:80 -H "Content-Range: bytes 0-1/11" 66 | 67 | 获取服务器端文件/tmp/ab.log的[0, 1]区间内容的MD5,由响应头X-MD5带回MD5值得十六进制字符串。 68 | 69 | ### 2.8、按片比对 70 | 71 | 举例: 72 | 73 | curl -X GET -sv http://www.upload.com/check/tmp/ab.log -x 127.1:80 -H "Content-Range: bytes 0-1/11" -H "Content-MD5: 0123456789abcdef" 74 | 75 | 比对文件大小和区间MD5值一致,返回200,否则返回401。 76 | 77 | ## 3、服务器端配置 78 | 79 | 举例: 80 | 81 | server { 82 | listen 80; 83 | server_name *.upload.com; 84 | 85 | client_body_in_file_only off; 86 | client_max_body_size 4m; 87 | 88 | set $c_acl_token 0123456789abcdef0123456789abcdef; 89 | access_by_bgn cacltime; 90 | location ~ /(upload|check|merge|delete|size|md5|empty|override) { 91 | root /tmp/upload; 92 | content_by_bgn cupload; 93 | } 94 | } 95 | 96 | ## 4、客户端工具用法 97 | 98 | 用法: 99 | 100 | perl cupload.pl [sync=] src= des= ip= [host=] [timeout=] [step=] [loglevel=<1..9>] [verbose=on|off] 101 | 102 | 文件上传举例: 103 | 104 | perl cupload.pl src=aa.log des=/tmp/ab.log ip=127.1:80 verbose=on 105 | 106 | 文件夹上传举例: 107 | 108 | perl cupload.pl sync=on src=/tmp des=/tmp ip=127.1:80 verbose=on 109 | 110 | ## 5、客户端工具源码 111 | 112 | perl脚本文件:[cupload.pl](https://github.com/chaoyongzhou/XCACHE/blob/master/bgn_ngx/tool/cupload.pl "cupload.pl") 113 | 114 | -------------------------------------------------------------------------------- /xcache下载模块cdownload.md: -------------------------------------------------------------------------------- 1 | # xcache下载模块cdownload 2 | 3 | ## 1、背景 4 | 5 | xcache的下载模块cdownload支持断点续传,文件大小不限,支持http协议传输。 6 | 7 | 客户端下载工具以分片形式下载文件,分片文件名格式为:<目标文件名>.part_<起始偏移量>_<结束偏移量>_<原始文件长度>,分片文件的内容为原始文件[起始偏移量,结束偏移量)的范围,即左闭右开区间的内容。 8 | 9 | 比如,下载远端文件aa.log到本地的ab.log,分片范围为[100, 200),原始文件长度为1000字节,那么分片文件名为ab.log.part_100_200_1000。 10 | 11 | ## 2、服务器端访问 12 | 13 | ### 2.1、按片下载文件 14 | 15 | 举例: 16 | 17 | curl -X GET -svo aa.log http://www.download.com/download/tmp/ab.log -x 127.1:80 18 | 19 | 下载服务器端文件/tmp/ab.log到本地文件aa.log。 20 | 21 | ### 2.2、按片下载文件 22 | 23 | 举例: 24 | 25 | curl -X GET -sv aa.log http://www.download.com/override/tmp/ab.log -x 127.1:80 -H "Content-Range: bytes 0-1/11" 26 | 27 | 下载服务器端文件/tmp/ab.log的[0, 1]区间,内容存储到本地文件aa.log。 28 | 29 | ### 2.3、备份文件 30 | 31 | 举例: 32 | 33 | curl -X GET -sv http://www.download.com/backup/tmp/ab.log -x 127.1:80 34 | 35 | 备份服务器端文件/tmp/ab.log到指定的备份目录下,这里备份目录由配置参数c\_download\_backup\_dir指定。 36 | 37 | ### 2.4、删除文件 38 | 39 | 举例: 40 | 41 | curl -X GET -sv http://www.download.com/delete/tmp/ab.log -x 127.1:80 42 | 43 | 删除服务器端文件/tmp/ab.log。 44 | 45 | ### 2.5、获取文件大小 46 | 47 | 举例: 48 | 49 | curl -X GET -sv http://www.download.com/size/tmp/ab.log -x 127.1:80 50 | 51 | 获取服务器端文件/tmp/ab.log的大小,单位:字节,由响应头X-File-Size带回。 52 | 53 | ### 2.6、获取目录下的一个文件 54 | 55 | 举例: 56 | 57 | curl -X GET -sv http://www.download.com/finger/tmp -x 127.1:80 58 | 59 | 获取服务器端目录/tmp下的一个文件,由响应头X-File带回;如果没有文件,则响应头X-File缺失。 60 | 61 | ### 2.7、按片计算MD5 62 | 63 | 举例: 64 | 65 | curl -X GET -sv http://www.download.com/md5/tmp/ab.log -x 127.1:80 -H "Content-Range: bytes 0-1/11" 66 | 67 | 获取服务器端文件/tmp/ab.log的[0, 1]区间内容的MD5,由响应头X-MD5带回MD5值得十六进制字符串。 68 | 69 | ### 2.8、按片比对 70 | 71 | 举例: 72 | 73 | curl -X GET -sv http://www.download.com/check/tmp/ab.log -x 127.1:80 -H "Content-Range: bytes 0-1/11" -H "Content-MD5: 0123456789abcdef" 74 | 75 | 比对文件大小和区间MD5值一致,返回200,否则返回401。 76 | 77 | ## 3、服务器端配置 78 | 79 | 举例: 80 | 81 | server { 82 | listen 80; 83 | server_name *.download.com; 84 | 85 | client_body_in_file_only off; 86 | client_max_body_size 4m; 87 | 88 | set $c_acl_token 0123456789abcdef0123456789abcdef; 89 | access_by_bgn cacltime; 90 | 91 | location ~ /(download|check|delete|size|md5|ddir|finger|backup) { 92 | root /tmp/download; 93 | set $c_download_backup_dir /tmp/backup; 94 | content_by_bgn cdownload; 95 | } 96 | } 97 | 98 | ## 4、客户端工具用法 99 | 100 | 用法: 101 | 102 | perl cdownload.pl [syncfiles=] des= src= ip= [host=] [sleep=] [timeout=] [step=] [loglevel=<1..9>] [verbose=on|off] 103 | 104 | 下载文件举例: 105 | 106 | perl cdownload.pl src=aa.log des=/tmp/ab.log ip=127.1:80 verbose=on 107 | 108 | 下载目录举例: 109 | 110 | perl cdownload.pl sync=on src=/tmp des=/tmp/ ip=127.1:80 verbose=on 111 | 112 | ## 5、客户端工具源码 113 | 114 | perl脚本文件:[cdownload.pl](https://github.com/chaoyongzhou/XCACHE/blob/master/bgn_ngx/tool/cdownload.pl "cdownload.pl") 115 | 116 | 117 | -------------------------------------------------------------------------------- /XFS使用/XFS配置举例.md: -------------------------------------------------------------------------------- 1 | 8个ngx worker和12个xfs的配置举例如下: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /XCACHE和XFS间通信/XCACHE和XFS间通信.md: -------------------------------------------------------------------------------- 1 | # XCACHE和XFS间通信 2 | 3 | ## 1、驱动模型与数据流 4 | 5 | XCACHE将NGX和BGN融合后,形成双epoll + 协程调度器的驱动模型。XFS是BGN的一个分布式模块,依然由BGN的epoll +协程调度器驱动。 6 | 7 | 客户端请求由NGX epoll接入,与XFS的交互以及与回源方向的交互,则由BGN epoll负责。 8 | 9 | 10 | ![](01.png) 11 | 12 | 13 | 14 | 举例描述功能,其中端口号为示意,具体看配置: 15 | 16 | XCACHE侧: 17 | 18 | * NGX 80/443端口: 走HTTP/HTTPS协议,负责与客户端的交互 19 | 20 | * BGN 818端口:走BGN协议,负责与XFS和CONSOLE交互,以及其它走BGN协议的端交互 21 | 22 | * BGN 918端口:走HTTP~~/HTTPS~~协议,负责NGX REST API交互 23 | 24 | 25 | 26 | XFS侧: 27 | 28 | * BGN 618端口:走BGN协议,负责与XCACHE和CONSOLE交互,以及其它走BGN协议的端交互 29 | 30 | * BGN 718端口:走HTTP~~/HTTPS~~协议,负责XFS REST API交互 31 | 32 | 33 | ## 2、网络架构 34 | 35 | XCACHE将nginx worker进程改造成唯一可标识的。每个nginx worker拥有唯一的tcid,以及对应的物理传输网络的bgn端口和rest端口。 36 | 37 | XFS采用单盘单进程管理。每个XFS进程拥有唯一的tcid标识,以及对应的物理传输网络的bgn端口和rest端口。 38 | 39 | nginx worker进程和XFS进程之间两两建立长连接。节点内所有nginx worker进程看作一个资源池,所有XFS进程看作一个资源池,两个池之间遵从master-slave的计算(网络)模型。 40 | 41 | ![](02.png) 42 | 43 | 44 | 上图配置说明: 45 | 46 | tcid和物理传输网络映射关系配置: 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | master-slave计算(网络)模型配置: 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ## 3、建连过程 67 | 68 | 按启动先后顺序看: 69 | 70 | (1)NGX先启动,XFS后启动:XFS查看配置得知自己角色是master,尝试与所有角色是slave的NGX建连。 71 | 72 | (2)XFS先启动,NGX后启动:NGX查看配置得知自己角色是slave,尝试与所有角色是master的XFS建连。 73 | 74 | (3)NGX和XFS都启动后断连:此情况应尽力避免,需要靠第三方探测。 75 | 76 | ## 4、一致性哈希表 77 | 78 | 每个nginx worker维护一张独立的XFS节点的一致性哈希表。 79 | 80 | 实节点:每个XFS节点为一个实节点,由XFS的tcid唯一标识。 81 | 82 | 虚节点:每个实节点虚拟出若干虚节点(缺省配置为32个),虚节点由64bits的哈希值标识。这64bits的构成为:tcid 32bits,replica 16bits(虚节点数), salt 16bits(当前为实节点在数组中的位置)。哈希算法同ngx\_murmur\_hash2。(注:salt未来应改成缺省做法+可配值,作为出现哈希碰撞时的规避手段) 83 | 84 | nginx worker根据每个访问资源在XFS中的存储路径,首先判断是否为热点资源,若不是,则计算CRC32值,据此查找一致性哈希表中对应的活着的XFS节点,发起请求;若是热点资源,则随机挑选一个活着的XFS节点,发起请求。(注:热点资源选择XFS的策略未来会变更) 85 | 86 | ## 5、读写删 87 | 88 | ### 5.1、读文件 89 | 90 | 基于BGN的任务通信机制,nginx worker向特定XFS发起读文件请求: 91 | 92 | MOD_NODE_TCID(&recv_mod_node) = store_srv_tcid; 93 | MOD_NODE_COMM(&recv_mod_node) = CMPI_ANY_COMM; 94 | MOD_NODE_RANK(&recv_mod_node) = CMPI_FWD_RANK; 95 | MOD_NODE_MODI(&recv_mod_node) = 0;/*only one rfs or xfs*/ 96 | 97 | task_p2p(CMPI_ANY_MODI, TASK_DEFAULT_LIVE, TASK_PRIO_NORMAL, TASK_NEED_RSP_FLAG, TASK_NEED_ALL_RSP, 98 | &recv_mod_node, 99 | &ret, FI_cxfs_read_e, CMPI_ERROR_MODI, file_path, &store_offset, store_size, content_cbytes); 100 | 101 | task\_p2p为点对点任务通信(单播), recv\_mod\_node为接收端四元组,FI\_cxfs\_read\_e为XFS模块的接口cxfs\_read\_e标识,后面的参数依次为:文件路径(file\_path)、文件内偏移量(store\_offset)、文件读取长度(store\_size)、返回文件内容存放处(content\_cbytes)。 102 | 103 | ### 5.2、写文件 104 | 105 | 基于BGN的任务通信机制,nginx worker向特定XFS发起写文件请求: 106 | 107 | MOD_NODE_TCID(&recv_mod_node) = store_srv_tcid; 108 | MOD_NODE_COMM(&recv_mod_node) = CMPI_ANY_COMM; 109 | MOD_NODE_RANK(&recv_mod_node) = CMPI_FWD_RANK; 110 | MOD_NODE_MODI(&recv_mod_node) = 0;/*only one rfs or xfs*/ 111 | 112 | task_p2p(CMPI_ANY_MODI, TASK_DEFAULT_LIVE, TASK_PRIO_NORMAL, TASK_NOT_NEED_RSP_FLAG, TASK_NEED_NONE_RSP, 113 | &recv_mod_node, 114 | &ret, FI_cxfs_update_with_token, CMPI_ERROR_MODI, file_path, cbytes, auth_token); 115 | 116 | task\_p2p为点对点任务通信(单播), recv\_mod\_node为接收端四元组,FI\_cxfs\_update\_with\_token为XFS模块的接口cxfs\_update\_with\_token标识,后面的参数依次为:文件路径(file\_path)、文件内容(cbytes)、合并回源过程中获得的token(auth\_token)。 117 | 118 | 注意:XCACHE中写文件使用的是update接口,而不是write接口(FI\_cxfs\_write),是考虑到出现多个请求独立回源后写盘的情形,所以选择后面的覆盖前面的策略。 119 | 120 | ### 5.3、删文件/删目录 121 | 122 | 基于BGN的任务通信机制,nginx worker向节点内全体XFS发起删目录请求: 123 | 124 | cmon_md_id = TASK_BRD_CMON_ID(task_brd); 125 | 126 | cmon_count_nodes(cmon_md_id, &num); 127 | 128 | task_mgr = task_new(NULL_PTR, TASK_PRIO_NORMAL, TASK_NEED_RSP_FLAG, TASK_NEED_ALL_RSP); 129 | for(pos = 0; pos < num; pos ++) 130 | { 131 | CMON_NODE cmon_node; 132 | MOD_NODE recv_mod_node; 133 | 134 | cmon_node_init(&cmon_node); 135 | if(EC_FALSE == cmon_get_node_by_pos(cmon_md_id, pos, &cmon_node)) 136 | { 137 | cmon_node_clean(&cmon_node); 138 | continue; 139 | } 140 | if(EC_FALSE == cmon_node_is_up(&cmon_node)) 141 | { 142 | cmon_node_clean(&cmon_node); 143 | continue; 144 | } 145 | MOD_NODE_TCID(&recv_mod_node) = CMON_NODE_TCID(&cmon_node); 146 | MOD_NODE_COMM(&recv_mod_node) = CMPI_ANY_COMM; 147 | MOD_NODE_RANK(&recv_mod_node) = CMPI_FWD_RANK; 148 | MOD_NODE_MODI(&recv_mod_node) = 0;/*only one rfs or xfs*/ 149 | 150 | task_p2p_inc(task_mgr, 0, &recv_mod_node, 151 | &ret, FI_crfs_delete_dir, CMPI_ERROR_MODI, file_path); 152 | cmon_node_clean(&cmon_node); 153 | } 154 | task_wait(task_mgr, TASK_DEFAULT_LIVE, TASK_NOT_NEED_RESCHEDULE_FLAG, NULL_PTR); 155 | 156 | 这是BGN的组播任务通信。因为切片的缘故,NGX侧视角看到的任意一个缓存资源,在XFS侧都是以目录形式存在的,因此,XCACHE的文件删除和目录删除,都归结为XFS的目录删除。 157 | 158 | 考虑到一致性哈希的存在,节点内每个XFS都应该执行一次目录删除动作。示意代码中,首先收集所有活着的XFS信息,然后通过task\_p2p\_inc创建一个子任务(目录删除),再通过task\_wait将整个任务发射出去。至于每个子任务如何到达正确的XFS,则是BGN底层的事,不用管。 159 | -------------------------------------------------------------------------------- /BGN标识体系/BGN标识体系.md: -------------------------------------------------------------------------------- 1 | # BGN标识体系 2 | 3 | ## 1、总览 4 | 5 | BGN适用场景为并行和分布式,两个BGN节点之间要考虑如何通信的问题。 6 | 7 | 通信首先要解决两个问题:(1)如何寻址 (2)协议规范 8 | 9 | 换言之,在一张互通的网络内,每个通信的节点要唯一标识出来,彼此之间交流的信息格式要定义出来,如同internet的IP地址和TCP/IP协议。 10 | 11 | BGN将业务层网络和物理传输网络分离,上层业务组网并不直接依赖于IP地址和端口号,因此BGN需要建立一套独立的标识体系。 12 | 13 | 先看一条七层心跳日志: 14 | 15 | [2020-04-12 09:58:28.003][tid 3910989][co 0x7fccefa9bab8] isend req: from (tcid 10.10.6.18,comm 3910989,rank 0,modi 0) to (tcid 10.10.67.18,comm 0,rank 0,modi 0) with priority 2, type 3, tag 1, ldb 8, seqno a0a0612.0.2240, subseqno 0, func id 40000002a 16 | 17 | 这条日志描述的是源BGN节点向目标BGN节点发送心跳检测的通信要素: 18 | 19 | 源BGN节点: 20 | 21 | 任务通信子:10.10.6.18 22 | 通信子:3910989 23 | 进程组内进程标识:0 24 | 模块实例标识:0 25 | 26 | 目标BGN节点: 27 | 28 | 任务通信子:10.10.67.18 29 | (任意)通信子:0 30 | 进程组内进程标识:0 31 | 模块实例标识:0 32 | 模块接口标识(十六进制):40000002a 33 | 34 | 即,BGN节点的通信主要描述的是两个模块实例之间的通信,简称模块间通信。进一步,源BGN节点无需暴露发起任务请求的模块接口标识,任务请求和任务响应的匹配由任务序号(seqno)和子任务序号(subseqno)完成。 35 | 36 | BGN节点通信时的寻址由四元组决定: 37 | 38 | (tcid,comm,rank,modi) 39 | 40 | 即,任务通信子、通信子、进程、模块实例,下文描述。 41 | 42 | BGN节点通信的协议规范,本质是TLV格式定义,简化掉T和L,仅保留V,另文描述。 43 | 44 | 45 | ## 2、通信子(comm)和任务通信子(tcid) 46 | 47 | 通信子(comm,全称communicator)概念来源于MPI(Message Passing Interface),后者在超算中广泛应用,用来唯一标识一组进程,这组进程在分布式环境下,可以分散在不同的服务器上。 48 | 49 | 在BGN发展史上,曾经有段时间兼容MPI,所以引入通信子概念,与MPI保持一致;后来与MPI脱钩,通信子名存实亡,只取两个值(91:MPI COMM WORLD和 0: MPI COMM SELF);再后来,为了适应业务场景的需要,通信子退化为只表达同一台服务器上的一组进程,依然只取91和0两值;直到最近通信子完全与MPI脱钩,取值为这组进程的主进程(转发进程,FWD)的pid,以及表示任意通信子的0值。 50 | 51 | 所以,在BGN中,通信子(comm)表达的是同一台服务器上的一组进程。在长期具体的实践中,这组进程通常退化为只有一个,即FWD进程,没有遇到多个进程的场景,这是与超算场景不同之处。 52 | 53 | 任务通信子(tcid,全称task communicator identifier)主要是在业务层网络中唯一标识BGN节点,具有全局唯一性特征。它借助MPI通信子的概念,表达一组进程,因此架空了MPI的通信子,使得后者在一段时间内名存实亡。任务通信子一个重要的用途是完成业务层网络和物理传输网络的映射: 54 | 55 | 56 | 57 | 这里ipv4和bgn、rest就是tcid对应的IPv4地址和端口号。 58 | 59 | 任务通信子采用类似IPv4的点分形式表达,maski/maske分别为业务层网络的内网掩码和外网掩码,即BGN具备七层路由能力,由于实际应用还没有遇到这么复杂的场景,此处不表。 60 | 61 | XCACHE将NGX和BGN融合,BGN与XFS(也是BGN节点)保持长连接通信。由于NGX支持reload操作,因此存在新旧两个nginx worker拥有相同的任务通信子(tcid)、不同的通信子(comm)的暂态。 62 | 63 | ![](01.png) 64 | 65 | 66 | 从旧的nginx worker发出的请求,XFS能够正确响应回来,正是基于不同的通信子(comm)来区分的。在nginx reload时,旧nginx worker会立即关闭bgn和rest监听端口,新nginx worker开启bgn和rest监听端口,这是与nginx设计不同的地方:nginx在master进程开启所有监听端口,worker只是继承,而BGN将每个nginx worker赋予唯一的bgn和rest端口号,任务通信子(tcid)和通信子(comm)也不同。 67 | 68 | 69 | ## 3、进程组内进程标识(rank) 70 | 71 | 任务通信子(tcid)内的一组进程从0开始,唯一编号,名为rank作为进程标识,这个概念也源于MPI。比如有三个进程,依次为rank 0,rank 1,rank 2。 72 | 73 | ![](02.png) 74 | 75 | 76 | 77 | 这组进程只能看作一个整体,一损俱损,比如rank 1挂了,则认为整个任务通信子(tcid)都挂了,全体退出。 78 | 79 | 这组进程中,第一个进程(rank 0)为特殊进程,负责对内、对外转发任务,又称之为转发进程(FWD Process / FWD Rank)。在实际应用中,通常一个任务通信子(tcid)只拥有一个进程,就是转发进程。 80 | 81 | ## 4、模块标识和模块实例号(modi) 82 | 83 | BGN中一切皆是对象,模块就是其中一例。BGN定义模块为:一组数据以及对这组数据进行操作的集合。它强调两点:数据、操作。定义与C++ / JAVA中的类并无实质差异。 84 | 85 | BGN对所有暴露在分布式环境中的模块(分布式模块)唯一编号,形如MD_xxx,比如XFS的模块编号为27: 86 | 87 | #define MD_CXFS ((UINT32) 27) 88 | 89 | 在模块的操作集合中,有两个操作是必须要提供的,即模块的启(start)、停(end),比如XFS的启停操作接口: 90 | 91 | UINT32 cxfs_start(const CSTRING *sata_disk_path, const CSTRING *ssd_disk_path) 92 | void cxfs_end(const UINT32 cxfs_md_id) 93 | 94 | 模块的启动创建模块的一个实例,返回该实例的编号,这就是实例号(modi),模块的所有其它操作均以该实例号作为第一个参数,相当于将C++ / JAVA类中接口的this指针释放出来了。 95 | 96 | 对模块实例进行编号,而不是模块实例的指针,这是在分布式环境下寻址的需要,一来指针传输到远端并无实际意义,二来内存的复用性使得指针有错误使用的风险。模块实例号的管理由独立的CBC(Central Board Control,中央控制板)进行。模块实例号在一个进程内唯一,通过模块实例号、模块类型,就可以从CBC查找到模块实例指针。 97 | 98 | 模块的停止就是销毁指定的模块实例,并将模块实例编号归还给CBC。 99 | 100 | 为了保持简洁性,BGN表述中经常不区分模块、模块实例、模块对象这些概念,请根据上下文辨别,不要学究。 101 | 102 | ## 5、模块接口标识(func id/func index) 103 | 104 | 分布式模块中的所有操作都会唯一编号,由模块类型和操作接口序号组成,全网唯一,形如FI_xxx: 105 | 106 | #define FI_cxfs_start ((UINT32) ((MD_CXFS << (WORDSIZE/2)) + 2)) 107 | #define FI_cxfs_end ((UINT32) ((MD_CXFS << (WORDSIZE/2)) + 3)) 108 | 109 | 模块类型在前半部的高比特位,接口序号在后半部的低比特位。(特别提醒:UINT32表示机器字长,而不是32bits数据类型。在64位系统上,UINT32代表64bits无符号长整型) 110 | 111 | 细心的读者可能发现了:为什么模块实例号没有携带模块类型信息,而接口标识中携带了呢? 112 | 113 | 这主要是设计失误。比较牵强的理由是,BGN始于32位系统,如果拿出16比特放模块类型,其支持的模块类型只有65536个,支持的实例数也只有65534个(去掉两个特殊的: ERR MODI, ANY MODI),而BGN期望模块无限可扩展,实例数无上限。时至今日回头看,受限于业务场景、部署方案、硬件限制,这样的理由不成立,16比特足够用。 114 | 115 | 模块类型信息放进模块实例号中是更合理的设计。今天我们修正这个设计失误,其实除了废点体力,并无任何技术障碍,目前是将错就错的状态。 116 | 117 | ## 6、内存类型标识 118 | 119 | BGN采用条状管理动态内存,每个内存类型都有一个内存管理器,从系统中一次申请多个,连续内存排列,BGN申请时给一个,批发-零售模式。 120 | 121 | 每个需要动态管理的内存类型都唯一编号(建议全网唯一),形如MM_xxx,在分配与回收内存时使用,比如 122 | 123 | #define MM_CXFS_WAIT_FILE ((UINT32)280) 124 | 125 | CXFS_WAIT_FILE *cxfs_wait_file; 126 | 127 | alloc_static_mem(MM_CXFS_WAIT_FILE, &cxfs_wait_file, LOC_CXFS_0013); 128 | free_static_mem(MM_CXFS_WAIT_FILE, cxfs_wait_file, LOC_CXFS_0015); 129 | 130 | 131 | ## 7、参数方向与参数类型标识 132 | 133 | 在两个BGN节点间通信时,需要按照协议规范对数据编码,参数方向和参数类型标识是重要的依据。 134 | 135 | 接口的参数方向有三种可能:输入(IN)、输出(OUT)、输入输出(IO)。 136 | 137 | 比如, 138 | 139 | EC_BOOL cxfs_read_e(const UINT32 cxfs_md_id, const CSTRING *file_path, UINT32 *offset, const UINT32 max_len, CBYTES *cbytes); 140 | 141 | 这里,带const修饰的都是输入参数(cxfs\_md\_id, file\_path, max\_len),不带const修饰的可能是输出参数,也可能是输入输出参数,比如offset是输入输出参数,cbytes是输出参数。 142 | 143 | 通常,一个输入输出参数可以拆解成一个输入参数 + 一个输出参数,这样用const就可以区分开,否则需要人工界定。保持规范性,有利于工具的使用,比如生成下面的接口定义信息: 144 | 145 | { 146 | /* -- EC_BOOL cxfs_read_e(const UINT32 cxfs_md_id, const CSTRING *file_path, UINT32 *offset, const UINT32 max_len, CBYTES *cbytes); -- */ 147 | /*func module */ MD_CXFS, 148 | /*func logic addr */ (UINT32)cxfs_read_e, 149 | /*func beg addr */ 0, 150 | /*func end addr */ 0, 151 | /*func addr offset*/ 0, 152 | /*func name */ "cxfs_read_e", 153 | /*func index */ FI_cxfs_read_e, 154 | /*func ret type */ e_dbg_EC_BOOL, 155 | /*func para num */ 5, 156 | /*func para direct*/ {E_DIRECT_IN,E_DIRECT_IN,E_DIRECT_IO,E_DIRECT_IN,E_DIRECT_OUT,}, 157 | /*func para type */ {e_dbg_UINT32,e_dbg_CSTRING_ptr,e_dbg_UINT32_ptr,e_dbg_UINT32,e_dbg_CBYTES_ptr,}, 158 | /*func para val */ 0, 0, {0}, 159 | }, 160 | 161 | 这是通信协议约定的一部分,模块接口约定,它最终影响到编码、解码、构建调用场景时实参与形参结合的方式,非常重要,不能出现丝毫错误。 162 | 163 | 在模块接口约定中,出现了形如e\_dbg\_xxx的接口参数类型标识,这也是唯一标识的。每种需要通信的数据类型都拥有一个标识,它与内存类型标识不同:前者用于通信,后者用于内存管理。当然,参数类型标识必然要映射到内存类型标识,因为凡是涉及到通信的内存,必然需要动态内存管理。 164 | 165 | ## 8、位置标识 166 | 167 | 在第6节举例中,出现了LOC\_CXFS\_0013和LOC\_CXFS\_0015,形如LOC\_xxx\_xxxx的就是位置标识,用来唯一标识出现在代码中的位置。 168 | 169 | #define LOC_CXFS_0013 ((UINT32)( 1421)) 170 | #define LOC_CXFS_0014 ((UINT32)( 1422)) 171 | #define LOC_CXFS_0015 ((UINT32)( 1423)) 172 | 173 | 位置标识与代码高度相关,它的目的是为了跟踪、排查问题,比如内存泄漏、流程流转等。至于为什么不使用\_\_FUNCTION\_\_,\_\_LINE\_\_之类预编译宏,主要是考虑到记录问题,字符串不好记录、两个值不好记录,所以用位置标识唯一化了。 174 | 175 | 位置标识唯一化通常由辅助工具完成,可以在编译前执行工具。 176 | -------------------------------------------------------------------------------- /seastar/seastar DEMO - 神奇的return.md: -------------------------------------------------------------------------------- 1 | # 1 神奇的return 2 | 3 | seastar对于future的return处理十分独特,我没弄明白这是不是C++自身的特性,但实验结果很清楚。 4 | 5 | 首先回顾一下seastar源码分析5.2节 “future链式操作”。我们说, future1.then(lambda1).then(lambda2).then(lambda3),等价于, 6 | 7 | future2 = future1.then(lambda1) 8 | future3 = future2.then(lambda2) 9 | future4 = future3.then(lambda3) 10 | 11 | 以及5.4节“执行但不运行”,上述链式操作实际运行情况为: 12 | 13 | 先执行future1.then,将lambda1封装成一个task抛出,返回future2; 14 | 15 | 然后执行future2.then,将lambda2封装成一个task抛出,返回future3; 16 | 17 | 然后执行future3.then,将lambda3封装成一个task抛出,返回future4; 18 | 19 | 这行代码执行完,返回的是future4,而此时此刻lambda1,lambda2,lambda3一个都没执行! 20 | 21 | 好,我们来看示例: 22 | 23 | #include "core/app-template.hh" 24 | #include "core/reactor.hh" 25 | #include "core/sleep.hh" 26 | #include "core/thread.hh" 27 | #include "core/gate.hh" 28 | #include "core/temporary_buffer.hh" 29 | #include "core/iostream-impl.hh" 30 | 31 | #include "util/log.hh" 32 | 33 | #include 34 | #include 35 | 36 | using namespace seastar; 37 | 38 | using namespace std::chrono_literals; 39 | 40 | seastar::future<> entrance(); 41 | 42 | seastar::logger demo_logger("demo"); // 定义日志 43 | 44 | int main(int argc, char** argv) { 45 | seastar::app_template app; 46 | 47 | try { 48 | app.run(argc, argv, entrance); // run()接口表示entrance执行完毕,app结束 49 | demo_logger.debug("main: done"); // app结束后,打印日志 50 | } catch(...) { 51 | std::cerr << "Couldn't start application: " 52 | << std::current_exception() << "\n"; 53 | return 1; 54 | } 55 | return 0; 56 | } 57 | 58 | seastar::future<> entrance() { 59 | demo_logger.set_level(log_level::debug); // 设置日志级别为debug。注意:该设置不能发生在app.run()之前,因为configure还没执行。事实上,只能在这儿设置。 60 | 61 | demo_logger.debug("entrance: enter"); // sleep前打印一条日志,表示已进入entrance 62 | return seastar::sleep(5s).then([] { demo_logger.debug("entrance: done"); }); // sleep后打印一条entrance结束日志 63 | } 64 | 65 | 实际运行结果为 66 | 67 | DEBUG 2018-04-08 13:49:57,912 [shard 0] demo - entrance: enter 68 | DEBUG 2018-04-08 13:50:02,913 [shard 0] demo - entrance: done 69 | DEBUG 2018-04-08 13:50:02,957 [shard 0] demo - main: done 70 | 71 | 即,进入entrance,sleep 5秒后,打印entrance结束,回到main打印app结束。 72 | 73 | 现在,修改entrance()接口如下: 74 | 75 | seastar::future<> entrance() { 76 | demo_logger.set_level(log_level::debug); 77 | 78 | demo_logger.debug("entrance: enter"); 79 | seastar::sleep(5s).then([] { demo_logger.debug("entrance: done"); }); // 注意,这条语句前的return删除了 80 | 81 | demo_logger.debug("entrance: leave"); // 打印日志,表示准备离开entrance 82 | return seastar::make_ready_future<>(); //返回一个ready future 83 | } 84 | 85 | 改动之处在于,将return挪走了。运行结果为 86 | 87 | DEBUG 2018-04-08 13:57:34,165 [shard 0] demo - entrance: enter 88 | DEBUG 2018-04-08 13:57:34,165 [shard 0] demo - entrance: leave 89 | DEBUG 2018-04-08 13:57:34,195 [shard 0] demo - main: done 90 | 91 | 程序根本没有等到sleep结束就返回了。 92 | 93 | 为什么仅仅挪到了一下return的位置,程序运行结果差别这么大? 94 | 95 | 根据seastar源码分析,代码 96 | 97 | seastar::sleep(5s).then([] { demo_logger.debug("entrance: done"); }); 98 | 99 | 是指,抛出一个sleep任务,抛出一个lambda(执行打印)任务。sleep任务结束后,开始执行lambda任务。这条语句执行完时,两个任务还没有开始“运行”。 100 | 101 | 那么,第二个程序的结果才是“符合预期的”:立即返回main结束! 102 | 103 | 为什么第一个程序非要等这两个任务运行完毕,才从entrance()返回?为什么第二个程序压根儿不等两个任务运行,就从entrance()返回了? 104 | 105 | 这就是神奇的return的功劳:如果需要return一个future,那么仅当其变成ready future时,才会执行return动作! 106 | 107 | 换言之,return是一个future阻塞点( future-based blocking point)。 108 | 109 | 现在不难理解上面两个程序的运行结果差异了:第一个程序抛出任务,阻塞,等待任务执行完成后返回;第二个程序抛出任务后不管了,直接返回。 110 | 111 | 事实上,如果将app.run()改成app.run\_deprecated()接口,那么第二个程序会在sleep 5秒后,打印结果,即两个任务有机会运行。 112 | 113 | 继续改造entrance(),现在将sleep语句放进do_with里,类上,比较return点不同时的运行差异: 114 | 115 | seastar::future<> entrance() { 116 | demo_logger.set_level(log_level::debug); 117 | 118 | demo_logger.debug("entrance: enter"); 119 | 120 | return seastar::do_with(5, [](auto& unused) { 121 | return seastar::sleep(5s).then([] { demo_logger.debug("entrance: done"); }); // return点在此 122 | }).then([]{ demo_logger.debug("entrance: do_with then"); }); // do_with返回时,打印一条日志 123 | } 124 | 125 | 运行结果: 126 | 127 | DEBUG 2018-04-08 14:20:22,282 [shard 0] demo - entrance: enter 128 | DEBUG 2018-04-08 14:20:27,282 [shard 0] demo - entrance: done 129 | DEBUG 2018-04-08 14:20:27,283 [shard 0] demo - entrance: do_with then 130 | DEBUG 2018-04-08 14:20:27,315 [shard 0] demo - main: done 131 | 132 | 挪动return的位置: 133 | 134 | seastar::future<> entrance() { 135 | demo_logger.set_level(log_level::debug); 136 | 137 | demo_logger.debug("entrance: enter"); 138 | 139 | return seastar::do_with(5, [](auto& unused) { 140 | seastar::sleep(5s).then([] { demo_logger.debug("entrance: done"); }); 141 | return seastar::make_ready_future<>(); // return点在此 142 | }).then([]{ demo_logger.debug("entrance: do_with then"); }); 143 | } 144 | 145 | 运行结果: 146 | 147 | EBUG 2018-04-08 14:22:10,312 [shard 0] demo - entrance: enter 148 | DEBUG 2018-04-08 14:22:10,312 [shard 0] demo - entrance: do_with then 149 | DEBUG 2018-04-08 14:22:10,347 [shard 0] demo - main: done 150 | 151 | 没等sleep结束就返回了。原理同上。 152 | 153 | 继续改造entrance(),这次挪动在do\_with处的return: 154 | 155 | seastar::future<> entrance() { 156 | demo_logger.set_level(log_level::debug); 157 | 158 | demo_logger.debug("entrance: enter"); 159 | 160 | seastar::do_with(5, [](auto& unused) { 161 | return seastar::sleep(5s).then([] { demo_logger.debug("entrance: done"); }); 162 | }).then([]{ demo_logger.debug("entrance: do_with then"); }); 163 | 164 | demo_logger.debug("entrance: leave"); 165 | return seastar::make_ready_future<>(); // return点在此 166 | } 167 | 168 | 运行结果: 169 | 170 | DEBUG 2018-04-08 14:32:00,197 [shard 0] demo - entrance: enter 171 | DEBUG 2018-04-08 14:32:00,197 [shard 0] demo - entrance: leave 172 | DEBUG 2018-04-08 14:32:00,228 [shard 0] demo - main: done 173 | 174 | do\_with内的lambda没有运行,其后的then也没有运行。也就是说,do\_with抛出两个任务后,没等返回的future变成ready,就跑去执行下一条语句了。 175 | 典型的发射后不管。可见,do\_with前的return遵循同样的规则:**return是阻塞点**。 176 | 177 | 继续改造entrance,这次挪动do\_with内,sleep前的return: 178 | 179 | seastar::future<> entrance() { 180 | demo_logger.set_level(log_level::debug); 181 | 182 | demo_logger.debug("entrance: enter"); 183 | 184 | seastar::do_with(5, [](auto& unused) { 185 | seastar::sleep(5s).then([] { demo_logger.debug("entrance: done"); }); 186 | return seastar::make_ready_future<>(); // return点在此 187 | }).then([]{ demo_logger.debug("entrance: do_with then"); }); 188 | 189 | demo_logger.debug("entrance: leave"); 190 | return seastar::make_ready_future<>(); 191 | } 192 | 193 | 运行结果: 194 | 195 | DEBUG 2018-04-08 14:40:03,987 [shard 0] demo - entrance: enter 196 | DEBUG 2018-04-08 14:40:03,987 [shard 0] demo - entrance: do_with then 197 | DEBUG 2018-04-08 14:40:03,987 [shard 0] demo - entrance: leave 198 | DEBUG 2018-04-08 14:40:04,020 [shard 0] demo - main: done 199 | 200 | 不是说好的do\_with前没有return,抛出两个任务后不管了吗?为什么do\_with后的then这个任务被执行了呢? 201 | 202 | 这是由seastar::do\_with()的实现决定的:do\_with被调用时,立即执行lambda, 203 | 204 | (a) 如果执行结果返回一个ready future,则继续调用后面的then()接口,执行then()中的lambda; 205 | 206 | (b) 如果执行结果返回的future不ready,那么就抛出任务,然后调用后面的then()接口, then()中的lambda只能作为前面抛出任务的continuation,不能被执行。 207 | 208 | 深刻理解阻塞点、执行序、运行时机,不仅对理解seastar很重要,也是理解并行计算的重要一环。 -------------------------------------------------------------------------------- /XFS使用/依赖包安装.md: -------------------------------------------------------------------------------- 1 | # 1、概述 2 | 3 | 本文描述XCACHE、XFS、CONSOLE编译所需的依赖包的安装。因为编译后形成的安装包是自包含的,携带全部的依赖库,因此对依赖包的安装路径有要求。 4 | 5 | 6 | 依赖包包括: 7 | 8 | (1)openssl 9 | 10 | 社区主页:https://github.com/openssl/openssl 11 | 当前版本:1.1.1f 12 | 使用版本:1.1.1b。为了支持keyless功能,强烈建议使用董宇修改过的版本。 13 | 社区仓库:https://github.com/openssl/openssl/releases 14 | 社区下载:https://github.com/openssl/openssl/archive/OpenSSL_1_1_1b.tar.gz 15 | 16 | (2)luajit2 (**仅XCACHE需要**) 17 | 18 | 社区主页:https://github.com/openresty/luajit2 19 | 当前版本:2.1-20200102 20 | 社区仓库:https://github.com/openresty/luajit2/releases 21 | 社区下载:https://github.com/openresty/luajit2/archive/v2.1-20200102.tar.gz 22 | 23 | (3)pcre 24 | 25 | 社区主页: https://www.pcre.org/ 26 | 当前版本:8.44 27 | 社区仓库:ftp://ftp.pcre.org/pub/pcre 28 | 社区下载:ftp://ftp.pcre.org/pub/pcre/pcre-8.44.tar.gz 29 | 30 | 31 | (4)ncurses 32 | 33 | 社区主页:https://invisible-island.net/ncurses/announce.html 34 | wiki主页: https://en.wikipedia.org/wiki/Ncurses 35 | 当前版本:6.2 36 | 社区仓库:ftp://ftp.invisible-island.net/ncurses/ 37 | ftp://ftp.gnu.org/gnu/ncurses/ 38 | 社区下载:ftp://ftp.invisible-island.net/ncurses/ncurses-6.2.tar.gz 39 | ftp://ftp.gnu.org/gnu/ncurses/ncurses-6.2.tar.gz 40 | 41 | 42 | (5)readline 43 | 44 | 社区主页:https://tiswww.case.edu/php/chet/readline/rltop.html 45 | 当前版本:8.0 46 | 社区仓库: https://ftp.gnu.org/gnu/readline/ 47 | 社区下载: https://ftp.gnu.org/gnu/readline/readline-8.0.tar.gz 48 | 49 | (6)libxml2 50 | 51 | 社区主页:http://xmlsoft.org/ 52 | 当前版本:2.9.10 53 | 社区仓库:ftp://xmlsoft.org/libxml2/ 54 | https://gitlab.gnome.org/GNOME/libxml2/-/tags 55 | 社区下载 ftp://xmlsoft.org/libxml2/libxml2-2.9.10.tar.gz 56 | https://gitlab.gnome.org/GNOME/libxml2/-/tags/v2.9.10 57 | 58 | (7)expat 59 | 60 | 社区主页:https://libexpat.github.io/ 61 | wiki主页: https://en.wikipedia.org/wiki/Expat_(library) 62 | 当前版本:2.2.9 63 | 社区仓库: https://github.com/libexpat/libexpat/releases 64 | 社区下载: https://github.com/libexpat/libexpat/releases/download/R_2_2_9/expat-2.2.9.tar.gz 65 | 66 | 67 | # 2、XCACHE编译依赖包安装 68 | 69 | 完整步骤: 70 | 71 | S1 创建依赖包安装目录 72 | 73 | mkdir -p /usr/local/xcache/depend 74 | 75 | S2 编译安装openssl 76 | 77 | tar zxvf openssl_1_1_1b.tar.gz && cd openssl_1_1_1b \ 78 | && ./config shared --prefix=/usr/local/xcache/depend/openssl \ 79 | && make depend && make install \ 80 | && make clean \ 81 | && ./config --prefix=/usr/local/xcache/depend/openssl \ 82 | && make depend && make install 83 | 84 | S3 编译安装luajit2 85 | 86 | tar zxvf luajit2-2.1-20200102.tar.gz && cd luajit2-2.1-20200102 \ 87 | && make -j && make install PREFIX=/usr/local/xcache/depend/luajit 88 | 89 | S4 编译安装 pcre 90 | 91 | tar zxvf pcre-8.44.tar.gz && cd pcre-8.44 \ 92 | && ./configure --prefix=/usr/local/xcache/depend/pcre \ 93 | && make -j && make install 94 | 95 | S5 编译安装 ncurses 96 | 97 | tar zxvf ncurses-6.2.tar.gz && cd ncurses-6.2 \ 98 | && ./configure --prefix=/usr/local/xcache/depend/ncurses \ 99 | && make -j && make install 100 | 101 | S6 编译安装readline 102 | 103 | tar zxvf readline-8.0.tar.gz && cd readline-8.0 \ 104 | && ./configure --prefix=/usr/local/xcache/depend/readline \ 105 | && make -j && make install 106 | 107 | S7 编译安装libxml2 108 | 109 | apt-get install -y pkg-config && apt-get -y install zlib1g-dev \ 110 | && tar zxvf libxml2-2.9.10.tar.gz && cd libxml2-2.9.10 \ 111 | && sh autogen.sh \ 112 | && ./configure --prefix=/usr/local/xcache/depend/xml2 --without-python \ 113 | && make -j && make install 114 | 115 | S8 编译安装expat 116 | 117 | tar zxvf expat-2.2.9.tar.gz && cd expat-2.2.9 \ 118 | && ./configure --prefix=/usr/local/xcache/depend/expat \ 119 | && make -j && make install 120 | 121 | # 3、XFS编译依赖包安装 122 | 123 | 简化步骤: 124 | 125 | 依赖XCACHE已安装的依赖包 126 | 127 | mkdir /usr/local/xfs && cp -rp /usr/local/xcache/depend /usr/local/xfs/ && rm -rf /usr/local/xfs/depend/luajit 128 | 129 | 完整步骤: 130 | 131 | 132 | S1 创建依赖包安装目录 133 | 134 | mkdir -p /usr/local/xfs/depend 135 | 136 | S2 编译安装openssl 137 | 138 | tar zxvf openssl_1_1_1b.tar.gz && cd openssl_1_1_1b \ 139 | && ./config shared --prefix=/usr/local/xfs/depend/openssl \ 140 | && make depend && make install \ 141 | && make clean \ 142 | && ./config --prefix=/usr/local/xfs/depend/openssl \ 143 | && make depend && make install 144 | 145 | S3 编译安装 pcre 146 | 147 | tar zxvf pcre-8.44.tar.gz && cd pcre-8.44 \ 148 | && ./configure --prefix=/usr/local/xfs/depend/pcre \ 149 | && make -j && make install 150 | 151 | S4 编译安装 ncurses 152 | 153 | tar zxvf ncurses-6.2.tar.gz && cd ncurses-6.2 \ 154 | && ./configure --prefix=/usr/local/xfs/depend/ncurses \ 155 | && make -j && make install 156 | 157 | S5 编译安装readline 158 | 159 | tar zxvf readline-8.0.tar.gz && cd readline-8.0 \ 160 | && ./configure --prefix=/usr/local/xfs/depend/readline \ 161 | && make -j && make install 162 | 163 | S6 编译安装libxml2 164 | 165 | apt-get install -y pkg-config && apt-get -y install zlib1g-dev \ 166 | && tar zxvf libxml2-2.9.10.tar.gz && cd libxml2-2.9.10 \ 167 | && sh autogen.sh \ 168 | && ./configure --prefix=/usr/local/xfs/depend/xml2 --without-python \ 169 | && make -j && make install 170 | 171 | S7 编译安装expat 172 | 173 | tar zxvf expat-2.2.9.tar.gz && cd expat-2.2.9 \ 174 | && ./configure --prefix=/usr/local/xfs/depend/expat \ 175 | && make -j && make install 176 | 177 | 178 | 179 | # 4、CONSOLE编译依赖包安装 180 | 181 | 简化步骤: 182 | 183 | 依赖XCACHE已安装的依赖包 184 | 185 | mkdir /usr/local/console && cp -rp /usr/local/xcache/depend /usr/local/console/ && rm -rf /usr/local/console/depend/luajit 186 | 187 | 完整步骤: 188 | 189 | 190 | S1 创建依赖包安装目录 191 | 192 | mkdir -p /usr/local/console/depend 193 | 194 | S2 编译安装openssl 195 | 196 | tar zxvf openssl_1_1_1b.tar.gz && cd openssl_1_1_1b \ 197 | && ./config shared --prefix=/usr/local/console/depend/openssl \ 198 | && make depend && make install \ 199 | && make clean \ 200 | && ./config --prefix=/usr/local/console/depend/openssl \ 201 | && make depend && make install 202 | 203 | S3 编译安装 pcre 204 | 205 | tar zxvf pcre-8.44.tar.gz && cd pcre-8.44 \ 206 | && ./configure --prefix=/usr/local/console/depend/pcre \ 207 | && make -j && make install 208 | 209 | S4 编译安装 ncurses 210 | 211 | tar zxvf ncurses-6.2.tar.gz && cd ncurses-6.2 \ 212 | && ./configure --prefix=/usr/local/console/depend/ncurses \ 213 | && make -j && make install 214 | 215 | S5 编译安装readline 216 | 217 | tar zxvf readline-8.0.tar.gz && cd readline-8.0 \ 218 | && ./configure --prefix=/usr/local/console/depend/readline \ 219 | && make -j && make install 220 | 221 | S6 编译安装libxml2 222 | 223 | apt-get install -y pkg-config && apt-get -y install zlib1g-dev \ 224 | && tar zxvf libxml2-2.9.10.tar.gz && cd libxml2-2.9.10 \ 225 | && sh autogen.sh \ 226 | && ./configure --prefix=/usr/local/console/depend/xml2 --without-python \ 227 | && make -j && make install 228 | 229 | S7 编译安装expat 230 | 231 | tar zxvf expat-2.2.9.tar.gz && cd expat-2.2.9 \ 232 | && ./configure --prefix=/usr/local/console/depend/expat \ 233 | && make -j && make install 234 | 235 | 236 | -------------------------------------------------------------------------------- /面向对象C编程/面向对象C编程.md: -------------------------------------------------------------------------------- 1 | 目录 2 | 3 | 1 前言 4 | 5 | 2 面向对象 6 | 7 | 2.1 结构体 8 | 9 | 2.2 枚举缺陷 10 | 11 | 2.3 释放出this指针 12 | 13 | 2.4 宏是接口 14 | 15 | 2.4.1 唯一性 16 | 17 | 2.4.2 封装性 18 | 19 | 2.5 对象生命周期 20 | 21 | 2.6 参数方向 22 | 23 | 2.7 函数二元返回值 24 | 25 | 2.8 唯一编号 26 | 27 | 2.9 分布式模块 28 | 29 | 3 基础库 30 | 31 | 4 回调与触发 32 | 33 | 5 万物皆对象 34 | 35 | 6 设计模式 36 | 37 | 6.1 工厂模式 38 | 39 | 6.2 单例模式 40 | 41 | 6.3 模板模式 42 | 43 | 6.4 观察者模式 44 | 45 | 6.5 调停者模式 46 | 47 | 7 设计原则 48 | 49 | 7.1 单一职责原则(SRP) 50 | 51 | 7.2 开放封闭原则(OCP) 52 | 53 | 7.3 依赖倒转原则(DIP) 54 | 55 | 7.4 接口隔离原则(ISP) 56 | 57 | 7.5 里氏替换原则(LSP) 58 | 59 | # 1 前言 60 | 61 | BGN走过了漫长的18年岁月,而面向对象C编程我已运用了15年,因此C语言能不能面向对象编程这个问题,显然不是个问题。可以负责任地说,C语言不仅适合面向对象编程,而且强大到好玩。 62 | 63 | 面向对象编程是一种思想,不是一门语言。掌握方法精髓,不拘泥于形式,不生搬硬套,即便用shell,也能写出面向对象的感觉来。 64 | 65 | 在BGN代码量首次过10万行时,我已感受到一个人维护的压力。在反反复复的重构中,悟出面向对象的强大,直至目前在思想上和运用上成体系发展,支撑BGN的代码量估计超过200万行。 66 | 67 | BGN与面向对象的关系,十分类似Linux与C的关系,因需而生,因需而变,交汇融合。 68 | 69 | 决定写这篇文章并不容易,我至少犹豫了四、五年,因为简单,所以更加艰难,因为本质,所以更易忽视。即便作出决定,依然没有十足的把握能写透彻,权当抛砖引玉,版本迭代中完善,不希望读者将其看作是一份编程规范。 70 | 71 | 谨以小文,作为BGN的18岁成人礼,纪念逝去的青春,纪念我的技术人生。 72 | 73 | 74 | # 2 面向对象 75 | 76 | 本文无意区分对象、类、模块、实例这些概念,请读者根据上下文自行断定,以免学究,流于形式。姑且对它们下个统一的定义:一组数据以及对这组数据进行操作的集合。 77 | 78 | ## 2.1 结构体 79 | 80 | 为了便于组织数据,C语言发明了结构体,伴随而来的是层次关系,这是表达面向对象中继承性的重要手段之一。 81 | 82 | 结构体中的成员用不同的名称区分,在内存中顺序排列,与之对应的联合体,则是共用内存。 83 | 84 | 定义结构体时,建议根据机器字长,手动对齐,不必劳烦编译器。个人喜好按8字节对齐,同时适用于32位系统和64位系统。 85 | 86 | ## 2.2 枚举缺陷 87 | 88 | 枚举类型是鸡肋,给编译器做类型检测用,其功效完全可以用带类型约束的宏常量代替。 89 | 90 | 枚举的缺陷在于其字节数的不确定性,存在误判的风险,比如, 91 | 92 | typedef enum 93 | { 94 | model_001 = 0, 95 | model_002 = 1, 96 | model_004 = 2 97 | }MODEL; 98 | 99 | 对该枚举类型取sizeof,通常结果是4字节。 100 | 101 | 修改一下, 102 | 103 | typedef enum 104 | { 105 | model_001 = 0, 106 | model_002 = 1, 107 | model_004 = 0x000000002 108 | }MODEL; 109 | 110 | 或者 111 | 112 | typedef enum 113 | { 114 | model_001 = 0, 115 | model_002 = 1, 116 | model_004 = (unsigned long long)0x000000002 117 | }MODEL; 118 | 119 | 我们期望sizeof结果是8字节,但依然是4字节。 120 | 121 | 鉴于此,历史上BGN一次性废弃了全部枚举类型,改为宏定义,比如可以改写成这样, 122 | 123 | #define MODEL_001 ((uint64_t) 0) 124 | #define MODEL_002 ((uint64_t) 1) 125 | #define MODEL_004 ((uint64_t) 2) 126 | 127 | ## 2.3 释放出this指针 128 | 129 | C++和JAVA语言中,类成员函数参数表的第一个隐含参数是this指针,表示当前对象。 130 | 131 | C语言自由奔放,不需要编译器准备该参数,可以纯手工添加,比如, 132 | 133 | void camd_process_pages(CAMD_MD *camd_md) 134 | 135 | void camd_process_page(CAMD_MD *camd_md, CAMD_PAGE *camd_page) 136 | 137 | 第一个参数camd_md表示当前模块CAMD_MD的一个实例化对象,相当于释放出this指针,函数名则表示对该对象的操作,或者该对象所执行的操作。 138 | 139 | 将对象放在首参位置是必须的、也是唯一的选择。 140 | 141 | 深入思考一下:对象是否必须由指针来表达? 142 | 143 | 答案是否定的,至少可以从两个方面来证明:其一,指针并非一定是内存地址,索引也可以看做是某种指针;其二,指针具有局部性,在单机单进程中有效,不能推而广之,在更大范围的分布式环境中有效。 144 | 145 | ## 2.4 宏是接口 146 | 147 | 宏,首先是字符串替换功能,为人熟知;其次是接口功能,用者寥寥,说不定我是原创者。 148 | 149 | 宏的接口功能在结构体成员的访问中,应用尤其频繁,举例, 150 | 151 | typedef struct 152 | { 153 | int fd; 154 | int rsvd; 155 | 156 | UINT32 *max_req_num; 157 | UINT32 cur_req_num; 158 | }CAIO_DISK; 159 | 160 | #define CAIO_DISK_FD(caio_disk) ((caio_disk)->fd) 161 | #define CAIO_DISK_MAX_REQ_NUM(caio_disk) ((caio_disk)->max_req_num) 162 | #define CAIO_DISK_CUR_REQ_NUM(caio_disk) ((caio_disk)->cur_req_num) 163 | 164 | 这是个有趣的话题:为什么要多此一举将结构体成员的访问封装到宏里? 165 | 166 | 主要是用到了宏的两个特性:唯一性、封装性。 167 | 168 | ### 2.4.1 唯一性 169 | 170 | 注意观察宏名规则:结构体名 + 成员名。在编码中适当规划一下,即可保证宏名的唯一性。这在后续的维护中、以及读码中会获益良多。 171 | 172 | 比如,要修改一个成员名称,那么找到所有该宏名出现的地方修改一次即可;增加或删除某个成员,手法类似;要查找某个成员在哪些地方使用的,查宏名即可,不必担心局部变量名和成员名重名的干扰。 173 | 174 | ### 2.4.2 封装性 175 | 176 | 仔细看宏接口的定义,它实际表达的是一个对象的操作:宏名是操作,参数是对象。典型的面向对象手法,与函数接口类似。换言之,对该结构体的所有最基本操作,封装进了宏接口中。 177 | 178 | 设想一下,如果上面的max_req_num类型不是指针类型,而是UINT32类型,已码完的代码些许变动即可适配,封装的优势明显。 179 | 180 | 事实上,宏接口在BGN中的应用遍地都是,令人发指。 181 | 182 | ## 2.5 对象生命周期 183 | 184 | 对象的生命周期管理通常严格划分为四个阶段:创建、初始化、清理和释放。举例, 185 | 186 | CAIO_DISK *caio_disk_new(); /*创建对象CAIO_DISK*/ 187 | 188 | EC_BOOL caio_disk_init(CAIO_DISK *caio_disk); /*初始化对象CAIO_DISK*/ 189 | 190 | EC_BOOL caio_disk_clean(CAIO_DISK *caio_disk); /*清理对象CAIO_DISK*/ 191 | 192 | EC_BOOL caio_disk_free(CAIO_DISK *caio_disk); /*释放对象CAIO_DISK*/ 193 | 194 | 接口调用遵循原则:创建时立即初始化,释放前先清理。 195 | 196 | 对于静态对象生命周期管理,可以只保留初始化和清理两个阶段。 197 | 198 | ## 2.6 参数方向 199 | 200 | 函数的参数分为三个方向:入参,出参,出入参。用来指示参数值的变化与否。 201 | 202 | 入参是指只传入、不修改;出参是指只传出、不传入;出入参是指先传入,后传出。 203 | 204 | 通常,要尽量减少出入参的使用,能不用就不用,比如拆成一个入参、一个出参来代替,除非万不得已。 205 | 206 | 这是因为,入参可以在类型前面加const、出参不加const来标示,而出入参不方便与出参区分,辨识度差。 207 | 208 | 如果非要增加辨识度,可以手动加空宏定义,比如IN、OUT、IO之类的,在代码量大的情况下,啰嗦又臃肿,不建议。 209 | 210 | ## 2.7 函数二元返回值 211 | 212 | 函数看作是对象的某种操作,函数二元返回值是指,函数的返回值只表达该操作成功或者失败,其它要表达的返回值可以由出参带出。 213 | 214 | 强烈建议函数采用二元返回值模式,为什么呢? 215 | 216 | 函数要么没有返回值,要么有两个及以上的返回值。函数作为操作,其返回值必然为后续逻辑提供决策依据,也就是说至少要分出两个分支。 217 | 218 | 随着函数调用深度的增加,分支数呈指数增长,何不选个增长最缓慢的模式呢?而且,采用减枝的方式,将失败分支作为异常回滚处理,可以有效降低复杂度,提高系统健壮性和生存能力。 219 | 220 | ## 2.8 唯一编号 221 | 222 | 这个话题太大,不打算展开。在一个系统中,给对象唯一编号,可以方便对象的索引。在分布式系统中,唯一编号必不可少,内存指针局部性导致其不可向外传递,而唯一编号可以。 223 | 224 | 在分布式系统中,给对象唯一编号并不容易,无中心的系统可以采用一定的规则,用随机数编号;也可以建立多层级的、立体式的编号,比如分布式系统中每个节点唯一编号、每个节点中的进程唯一编号、每个进程中的对象按规则唯一编号、对象的每个操作唯一编号等。 225 | 226 | ## 2.9 分布式模块 227 | 228 | 前面介绍了释放this指针的方式,来表达面向对象的手法。推广一下,将this指针替换成对象的唯一编号,即可在分布式环境中使用,成为分布式模块。 229 | 230 | 分布式模块之间的通讯无非是为了回答一个哲学问题:我是谁?我要到哪里去?我要干什么? 231 | 232 | 第一个问题由本端对象的唯一编号解决;第二个问题由对端对象的唯一编号解决;第三个问题由对端对象接口的唯一编号解决。其余的都是需要传递的参数数据。 233 | 234 | # 3 基础库 235 | 236 | 个人以为,限制使用者想像力、阻碍C成为面向对象编程语言的罪魁祸首,恐怕就是GLIBC了,看起来像初中生、高中生的水准。似乎也没人有兴趣重写一套GLIBC,只能DIY一些基本的面向对象的算法库,比如双向链表、队列、堆栈、数组、向量、红黑树、位图、字符串处理、字节处理、编解码器、内存管理器、日志组件、通信组件、信号处理、定时器处理等等。基础库提供友好接口,为上层提供可靠保障。上层实现与基础库分离,基础库的变更不影响上层逻辑,双方通过约定的接口交互。 237 | 238 | # 4 回调与触发 239 | 240 | 回调是经典用法,充分体现了C快速、简洁的特点,因为回调也是调用。 241 | 242 | 回调也并非完美无瑕,首当其冲的是代码可读性下降、复杂度上升、传参麻烦、变量生命周期管理麻烦,其次是局部特性,不能在分布式环境中直接使用。 243 | 244 | 回顾一下通信模型。通信有收有发,通常成对出现,比如给对端发送一条请求,对端回送一条响应,请求-响应模型。一次函数调用由函数接口、参数表、返回值(函数返回值以及出参)三部分构成,可谓浓缩版通信模型:函数接口+参数表视为请求,返回值视为响应。换言之,回调可以被通信模型代替。 245 | 246 | 再审视对象之间的关系。在现实世界中,继承性并非唯一,两个对象之间可以是上下继承关系、左右平行关系、互相包含关系。如何表达任意两个对象发生关系、相互作用,是个难题。 247 | 248 | 想象一下,将对象所有支持的操作视为触角,外界与对象的交互通过触角进行,外界给一个输入,对象给一个反馈,还是经典的冯洛伊曼模型。外界与对象的交互由通信模型实现。每个对象伸出触角,接受来自任意其它对象的交互请求并给出响应。脑补一下:有无数的对象伸出触角,认识的、不认识的都可以互相交互,构成一个神经网络。 249 | 250 | 这种方式,我称之为触发。 251 | 252 | # 5 万物皆对象 253 | 254 | 回顾一下我们给对象的定义,两个核心要素:数据、操作。可见,对象是抽象的概念,无论是客观存在的事物,还是主观臆断的想象,皆可视为对象。 255 | 256 | 对象是思考问题的落脚点,我们关心的是它的内涵与外延:对象是什么、能提供什么样的功能,与该对象交互的是谁、如何交互。 257 | 258 | 但凡好的系统架构与设计,都进行了合理的对象划分,满足基本的高内聚、低耦合的要求,同时兼顾系统的健壮性、可维护性、可扩展性。 259 | 260 | 对象不可无限细分,即便面向对象思想允许这么做,无节制地细分对象只会掉进复杂逻辑陷阱,终极考量是业务特点,彼此契合、恰到好处才是上策,不可一概而论。 261 | 262 | # 6 设计模式 263 | 264 | 面向对象思想出道以来,已经出现数十种设计模式,面向对象C编程可以完全覆盖,但的确没有必要生搬硬套。我们不分类,挑几个设计模式聊聊。 265 | 266 | ## 6.1 工厂模式 267 | 268 | 虚构造和多态可不必考虑,C中通过ctrl +c &ctrl +v就能解决的问题,不用搞那么复杂,徒增烦恼。 269 | 270 | 结合2.8 唯一编号,我们可以将不同的对象理解成不同的唯一编号:同一份模块代码,接口的首参为对象唯一编号,不同的编号就是不同的对象。对象编号可以由一个统一的编号池来分配管理。 271 | 272 | ## 6.2 单例模式 273 | 274 | 单例模式是工厂模式的特例,比如对象编号可以限定为0,无需动态分配,也禁止动态分配。典型的单例比如超类(超模块)。 275 | 276 | ## 6.3 模板模式 277 | 278 | 抽象出操作的骨架,虽然对于C而言是举手之劳,但属于高端用法,业务层面建议尽量少用或者不用,在底层系统层面可以考虑适当使用,其弊端在于代码的可读性下降不止一点两点。 279 | 280 | ## 6.4 观察者模式 281 | 282 | 也即订阅/发布模式,描述一对多的对象关系:多个订阅者监听一个发布者,交互由触发完成。 283 | 284 | ## 6.5 调停者模式 285 | 286 | 调停者模式用来描述星型结构的对象关系,对象之间交互通过调停者中转,所以又称中介者模式。这种模式适用于交互需要收敛的地方,交互由触发完成。 287 | 288 | 289 | # 7 设计原则 290 | 291 | 面向对象遵循五大基本原则: 292 | 293 | ## 7.1 单一职责原则(SRP) 294 | 295 | 即功能要单一,不要搞大而全的东西,将未来变动影响限制在最小范围。 296 | 297 | ## 7.2 开放封闭原则(OCP) 298 | 299 | 即对象接口可以扩展,但是不能变更。这是管控风险的基本手段。 300 | 301 | ## 7.3 依赖倒转原则(DIP) 302 | 303 | 即要针对接口编程,而不是针对实现编程。这是面向对象编程思维训练的第一步。 304 | 305 | ## 7.4 接口隔离原则(ISP) 306 | 307 | 使用多个专门的接口比使用单一的总接口要好,不同的功能使用不同的接口,把依赖降到最低,防止接口污染、对象功能污染。同时,这也是管控风险的基本手段。 308 | 309 | ## 7.5 里氏替换原则(LSP) 310 | 311 | 任何基类可以出现的地方,子类一定可以出现,这是继承复用的基本保障。在C语言中,不是特别强调该原则,但复用原则是必须遵守的,我们可用简单的ctrl+c & ctrl+v来代替该原则,好好利用宏接口以及函数接口即可,不必基类子类这般生硬。 312 | -------------------------------------------------------------------------------- /关于存储/关于存储.md: -------------------------------------------------------------------------------- 1 | # 关于存储 目录 1 本文目的 2 存储是什么 3 存储模型设计与对比 4 存储性能分析 5 存储实测启示 # 1 本文目的 2 | 本文试图解构存储的几种基本模型,来勾勒存储性能的约束因素、功能覆盖和未来趋势。 本文暂不涉及分布式存储相关,所述皆以单机存储为基本出发点。 3 | 鉴于存储体系的庞杂,本文不便一一阐述,寄希望于围绕若干主线分析讨论,不周之处在所难免。 # 2 存储是什么 4 | 本文所言存储是指,将数据以某种形式存储在持久化介质上,并提供持续稳定的访问。 存储设计与技术选型,通常与业务强关联。 比如Cache业务中,数据表现为文件或切片,存储介质表现为SATA盘、SSD盘,提供读、写、删三种基本操作,访问途径通常是API或者Restful API。注意不提供append操作,数据允许全部丢失。 * Restful API是指符合REST(Representational State Transfer,表现层状态转化)架构设计的API。 5 | * Restful架构具有如下特点 6 | (1) 每一个URI代表一种资源 (2) 客户端和服务器之间,传递这种资源的某种表现层 (3) 客户端通过HTTP动词(如GET,POST,PUT,DELETE,HEAD等),对服务器端资源进行操作,实现"表现层状态转化" * 经常性地,大家并没有遵循严格的Restful架构,但是严格遵循是应该的。 比如面向用户的存储业务中,基本操作可能会从文件扩展到目录,比如目录的创建、删除、列表等。 诸如文件追加写、文件重命名、文件移动、目录重命令、目录移动这类在POSIX中常见的操作,在存储设计中并不常见,对这些操作的支持,意味着整个存储设计会发生根本性改变。 # 3 存储模型设计与对比 7 | 所有的存储,本质上由两部分组成:name node和data node。 Name node的作用就是从文件路径映射到文件内容的实际存储位置: 8 | Name node: path -------→ location 比如,读文件前,我们把文件的完整路径告诉给name node,name node检索后将结果返回,这样我们就知道文件存储在哪块磁盘、哪个物理文件(可选)、偏移量是多少、文件长度是多少等信息: 9 | (disk_no, file_no, offset, len)= NameNode(path) 那么data node的作用又是什么呢?data node就是用来管理磁盘空间的。 比如,写文件时,我们把文件长度告诉给data node,data node经过一番折腾后返回,告诉我们在哪块磁盘、哪个物理文件(可选)、起始偏移量是多少的地方有所需要的空间: (disk_no, file_no, offset) = DataNode(file_size) 来看基本操作的流程: ## 3.1 读操作: 10 | (3.1.1)通过file path,检索name node,获得location信息 (3.1.2)访问location,获取文件内容 ## 3.2 写操作 11 | (3.2.1)通过file size,要求data node分配适当的空间,返回location (3.2.2)通过file path,检索并插入到name node,绑定file path和location (3.2.3)将文件内容写入到location指向的磁盘位置 ## 3.3 删操作 (3.3.1)通过file path,检索name node并删除file path,同时返回绑定的location信息 (3.3.2)通过location,要求data node回收对应的磁盘空间 形形色色的存储,就是由各种形态的name node和data node组合而成,我们考虑三种模型: 12 | ### 存储模型一(M1): 13 | 以RFS为代表的inode模型,即name node以树形结构存储完整的文件路径,data node管理块状磁盘设备。检索从name node根部开始,根据文件路径,沿树逐级查找。Data node管理整个磁盘空间的分配与回收。根据POSIX文件系统标准,inode大小不应小于255字节,考虑到树形结构组织的需要,必然大于256字节,这样,inode的实际大小最小就是512字节(对齐)。Inode的数量取决于所存储的文件的平均尺寸。 由于inode较大,name node需要的空间较大,data node需要的空间取决于page大小的设定,总的来说,大的空间需求量带来设计上的挑战。 ### 存储模型二(M2): 以HFS为代表的inode模型,即name node以树形结构存储完整文件路径的哈希值,data node管理块状磁盘设备。检索时,根据文件路径的哈希值,直接从name node中查找。Data node管理整个磁盘空间的分配与回收。理论上,Inode的大小可以控制在64字节,其中16字节存储哈希值。Inode的数量取决于所存储的文件的平均尺寸。 Inode较小,带来的红利是整个name node可以预先加载到物理内存,检索性能高;data node需要的空间取决于page大小的设定。 ### 存储模型三(M3): 以ATS为代表的极简模型,即name node以树形结构存储完整文件路径的哈希值,data node则退化为一个磁盘位置指针。检索时,根据文件路径的哈希值,直接从name node中查找。而data node仅负责提供写入时的磁盘位置信息,即磁盘空间的分配退化版,不再支持(精确)回收。理论上,Inode的大小可以控制在64字节,其中16字节存储哈希值。Inode的数量取决于所存储的文件的平均尺寸。 14 | Inode较小,可以把整个name node预先加载到物理内存,检索性能高;退化版的data node带来的红利是不占用空间,而且总是顺着磁道在当前位置插入,带来的代价是不支持空间的回收(把磁盘想象成一个环形存储,总是覆盖写)、以及每次读磁盘伴随着一次写磁盘(读取的数据插入到data node指向的当前磁盘位置)。 * 循环覆盖写是M3必备的特征,还是仅仅是ATS自己的实现方式?要回答这个问题, 15 | 我们可以从两个方面来入手: 16 | (1) 空间的分配与回收总是成对出现的 17 | > 这个问题反证即可:如果只有空间的分配,没有回收,那么空间的有限性决定了分配过程必然会在有限的时间内终止,这与实际应用场景不符。 大家的问题应该集中在回收谁、如何回收上。 > 对于M1而言,name node的inode较大,信息载荷较大(参见下面的信息熵),因此在name node中判定“回收谁”上比较容易,至于“如何回收”,就是data node算法的问题,M1的data node管理精细,可以做到精确回收。 > 对于M2而言,name node的inode较小,信息载荷较小,但在name node中判定“回收谁”还是可以做到的,M2的data node管理同样精细,可以做到精确回收。 > 对于M3而言,name node的inode较小,信息载荷较小,同样可以做到在name node中判定“回收谁”,但是M3的data node退化为一个表示当前位置的磁盘指针,已经没有办法做到精确回收,因此,在name node中判定“回收谁”没有意义了。或者说,对于M3而言,已经做不到精确判定“回收谁”,那么就降格为“随便回收”吧,而且只能由data node负责(注意:name node的判定没有意义,无须name node参与)。Data node只有一个磁盘位置信息,它的影响力也就在这个磁盘位置前后,因此data node磁盘位置指针稍微向前挪动一点,回收掉前面的数据即可,这也是它唯一的选择。 18 | (2) 香农熵或信息熵在计算机领域可以直接用比特数表示,一个比特表达的信息量严格小于两个比特表达的信息量 19 | > M3的data node退化为一个磁盘位置指针,比特数与M1和M2的data node不在一个量级上,这样的信息载荷不足以做到精确回收。 综上两点,循环覆盖写就是M3的必备特征,ATS的实现方式别无选择。 * 每次读磁盘一定伴随一次写磁盘吗? 未必。这与M3的具体实现有关。举例:从data node的磁盘位置指针出发,经过圆心划一条直线,将环形存储一份为二,如果读磁盘的区域落在与data node同属的半圆上,则不写磁盘;如果落在另半圆上,则写磁盘。这样,我们有50%的概率不写磁盘。从实现的角度来看,还要考虑覆盖写是否会引起数据被截断的不完整性等约束以及解决办法。从算法复杂度来看,还是指数级的,并没有下降到多项式级。 以上每种模型都有一种或多种实现方式,此处我们不关心实现。不妨依次将M1、M2、M3排成一行,我们来看“演进”过程: 20 | 从M1到M2:name node空间需求下降到1/8,目录管理和列表功能全部损失 从M2到M3:data node退化归零,磁盘管理功能损失一半 从M1到M3的“演进”过程,就是以功能损失换取空间下降的过程、随机读随机写降格到随机读追加写的过程,其它不在本文讨论范围内的存储模型也遵循同样的规律。 > 所谓“演进”,是为了方便做横向对比的一种说法,我们既可以沿着从M1到M3方向做对比,也可以反过来沿着从M3到M1方向做对比。每种模型都有自己的特点,不存在谁向谁“进化”的问题。 通常我们对比存储,是从功能与性能两方面对比分析,本小节限于功能对比,性能对比留给下小节。 先说从M1向M3“演进”的合理性:空间,尤其是物理内存空间,是稀缺的、有限的资源,而目录操作一般是低频操作,因此损失目录操作换取更小的内存占用,是合理、高性能的选择。对于Cache业务数据可丢失的特点,允许磁盘数据无预警覆盖,因此损失磁盘空间回收功能也是可以接受的。 再说从M1向M3“演进”的不合理性:内存价格每三年一个周期,下降是必然的,而且内存容量会继续增大,不排除被其他新技术取代的可能;目录操作虽然是低频操作,但却是用户或业务的“刚需”,损失掉的功能就需要以辅助的手段补回来,比如外挂数据库,本质上就是为了具备M1的name node的所有功能,付出流程代价。极小化磁盘空间分配功能、损失掉磁盘空间回收功能,则直接限定了存储的适用范围,代价不言而喻。 # 4 存储性能分析 21 | 相对于功能分析带有明显用户或业务需求而言,性能对比分析则复杂得多,我们需要实践与理论结合。 站在存储外面看性能,黑盒分析维度有: 22 | (1) Latency (2) QPS (3) Disk IO (4) Network IO (5) CPU Occupation (6) Mem Occupation (7) Connection Num 站在存储里面看设计对性能的影响,白盒分析维度有: 23 | (1) 内存管理 (2) 中断/用户态与内核态切换 (3) 顺序读写 (4) 裸盘管理 (5) AIO(异步IO) (6) DIRECT IO 我们做白盒分析。在此之前,有必要介绍一下pageCache的功效。 24 | pageCache本身是为了平衡高速内设(比如CPU、内存)与低速外设(如磁盘)之间的性能差异而产生的。内核从外设读数据时,先存进pageCache,然后再送给应用层;写数据时,先存进pageCache,再慢慢同步到外设。pageCache可以使用所有未被使用的物理内存页,物理内存不够时,可以通过释放干净页和同步脏页到外设的方式,淘汰一批pageCache。磁盘属于机械设备,不可避免地存在时延随机抖动,pageCache可以减少抖动,从而提供相对可接受的稳定访问。 ## 4.1 DIRECT IO 25 | 通常,对文件的读写主要经过应用层缓存(可选)、内核pageCache缓存、磁盘。如果开启DIRECT IO,则可以跳过pageCache。 那么,什么时候需要开启DIRECT IO呢?有两种可能: (1) 担心数据在pageCache中,不能及时同步到磁盘,导致pageCache数据和磁盘不一致。在诸如掉电等异常情况下,引起元数据、文件数据等毁坏。 (2) 在应用层开辟大内存空间,自行管理缓存。 可能(1)不予考虑。可能(2)是基于对业务的充分自信,相信自己的缓存与淘汰策略明显优于pageCache的策略。 ## 4.2 AIO(异步IO) 26 | 磁盘AIO是有争议的。SATA磁盘只有一个磁头,无论如何异步化,读写都需要移动磁头到指定位置,读写指定长度的数据,假设磁盘速率稳定,异步和同步耗时相同。在单进程单线程模型中,AIO不会有性能上的提升。在单进程多线程中,可以按照“生产者-消费者”模型,设立消费者线程专门读写磁盘,前端生产者线程负责网络数据收发,这又导致生产者和消费者之间的速率要匹配,否则生产者疯狂生产数据,消费者扛不住,因为耗时是固定不变的。反观POSIX文件系统,主要是在内核做读写操作的合并:磁盘上两个或多个相邻的数据的读或写可以合并为一次读或写,以减少磁头移动的次数和距离。 应用层磁盘AIO有点鸡肋的感觉,要发挥它的功效有难度。 ## 4.3 裸盘管理 27 | POSIX文件系统属于通用型,要考虑和支持各种情况,势必臃肿低效,而客户或业务、特别是通过restful API这类非标准POSIX API访问的存储需求,支持的接口相对较少,磁盘一般按照块设备管理,给定偏移量和长度就可以完成读写访问,这就给裸盘管理提供了理由。 ## 4.4 顺序读写 目前,一般都是把文件内容放在连续的磁盘空间中,对外不提供append操作接口和随机删改文件内容的接口。 各存储都会关注顺序读写,因此彼此性能差距可以忽略。 ## 4.5 中断/用户态和内核态切换 时钟中断是指我们需要通过时钟中断来获取当前时钟信息。频繁的时钟中断会降低系统性能,因此服务器普遍采用墙上时钟的方式来获取近似时钟、减少中断次数。存储服务器在时钟中断上实现相近,性能差距可以忽略。 缺页中断是指访问进程空间的某个地址时,发现没有映射到物理页,从而产生缺页中断,要求内核分配相应的内存页。如果要访问的地址是从磁盘映射到进程空间的,则还会触发内存与磁盘之间的数据交换。缺页中断触发的流程比较长,对性能影响比较大。换言之,如果能把数据全部放进物理内存,缺页中断彻底消失,性能最高。 系统调用中断是指通过系统调用,比如文件的read/write访问接口,从用户态切换到内核态。在用户态与内核态互相切换的过程中,必然伴随一次数据的内存拷贝。内存拷贝是耗时操作,对性能影响较大。 Intel针对intel ssd提供SPDK,允许用户直接在用户空间访问SSD盘,既减少了用户态和内核态之间的数据交换,又减少了冗长的中间环节。从intel实测数据和厂商数据来看,性能相当优异。 ## 4.5 内存管理 28 | 无论是name node和data node元数据的检索,还是存储软件在运行过程中产生的内存分配与回收,都离不开内存的管理。迄今为止,除了CPU,随机访问最快的就是内存,各存储都在内存的占用、使用上做足了文章。 通行的做法是,应用层申请一大块内存,自己进行分配与回收管理,这样可以最大程度减少缺页中断、内存碎片化。被访问最频繁的数据固定在内存,可以获得最高的性能提升。 事实上,抛开差异不明显的次要因素和实现因素,真正影响存储性能的因素有三个: 29 | (1) 内存大小 (2) 内存拷贝次数 (3) 磁盘IO次数 内存大小则是决定性因素:内存越大,内存命中率越高,中断/用户态与内核态切换越少,磁盘IO次数越少,整体性能自然越高。 从极限角度也可以理解为什么内存大小是决定性因素:内存看作磁盘的缓存,内存大到可以装下整个磁盘时,所有访问都可以在内存完成,性能最高;内存小到为零时,所有访问都必须落盘,性能最低。 当然,存储的性能并不与内存线性相关,它还与存储的实现方式和业务场景高度相关: 实际运行中,当数据经过一段时间的预热后,内存的增加可能不会再提升存储性能了。 # 5 存储实测启示 回顾RFS(M1型存储)和ATS(M3型存储)测试数据,我们可以看到RFS性能并不比ATS差,相反很多方面更胜一筹。在name node检索算法上,RFS和ATS的复杂度都是O(logn),不相上下;RFS的name node是ATS的8倍,不能全放在内存中,访问容易触发缺页中断,性能较ATS差距大;ATS启用DIRECT IO,直接与磁盘交互,RFS充分利用pageCache,数据预热后直接从pageCache获取;ATS采用单进程多线程AIO模式,RFS采用单进程多协程同步IO模式,估计差距不大;ATS读总伴随着一次写,RFS不存在额外写。 这样的测试结果和对比分析,留给我们许多启示 ## 5.1 启示一:M1型存储并非不可取,内存才是性能关键 30 | 内存是性能关键自不待言。从M3向M1方向“演进”的过程,就是一个功能逐渐增加的过程。从应用的角度来说,功能越丰富,外挂就越少,对业务的支持越方便。M1更高效合理地利用内存,也可以获得不俗的性能表现。 ## 5.2 启示二:DIRECT IO和AIO可有可无 31 | 如果是应用层内存缓存,DIRECT IO很有必要,数据不用在pageCache和应用层重复缓存,否则DIRECT IO不用开启。 对于AIO,不建议开启。 ## 5.3 启示三:内核的pageCache缓存机制没那么差 32 | 如果都是内存命中,应用层内存命中比pageCache命中路径更短、性能更高:少了一次系统调用中断和一次从内核态到用户态的数据拷贝。 33 | 如果都是内存未命中,则二者相差无几。 如果一个命中、另一个未命中呢?二者谁的命中率更大呢?从RFS和ATS实测结果推算,RFS的命中率更高一点,因为pageCache可以利用所有未被使用的物理内存,这也说明pageCache甄别数据热度的算法没那么差。 ## 5.4 启示四:用户空间效率高,轮询不比中断差 用户空间执行效率高这是不争的事实:缓存从内核pageCache移到用户空间,内存分配与回收从内核内存管理移到用户空间,协程可以认为是线程在用户空间的翻版,……,这一切的一切都在说明:人们受不了内核了,自己动手干吧! 更恐怖的是,intel的DPDK和SPDK:DPDK将网络IO从内核移到了用户空间,SPDK将SSD IO从内核移到了用户空间。 为什么说这事儿恐怖?因为以前的网络IO和磁盘IO是通过硬件中断、事件驱动的方式与应用层交互的。从内核移出后,相当于去掉了驱动机制:没人通知应用层什么时候干什么事了,应用层只能靠自己瞎忙活!应用层大招没有,土鳖的办法倒有一个:轮询。 事实证明,轮询的效率不比中断差。中断曾经是“众多调度方案中,表现最好的一个”,为什么现在不如轮询了呢? 真实的原因只有一个:外设不再是低速的代名词了!对比一下,intel SSD最新款Optane的随机读写性能仅仅比物理内存低20%。外设的高性能,使得不经过内核的平衡与调度,也能扛住应用层不停轮询,并提供很好的性能。 计算机硬件在悄然发生深刻变化,软件在演进,存储的架构设计也必然会随之发生变化。 -------------------------------------------------------------------------------- /NGX与BGN融合/NGX与BGN融合.md: -------------------------------------------------------------------------------- 1 | # NGX与BGN融合 目录 1 本文目的 2 名词解释 3 融合的目的 4 融合的技术路线 5 融合的技术障碍 6 合并回源 7 融合后前景展望 # 1 本文目的 截止目前,NGX和BGN融合后在线上运行稳定,尚未发现重大问题。 本文分析和总结NGX和BGN融合的原因、遇到的问题以及解决途径,希望对基于NGX开发的同仁们有所启发和借鉴。 业内尚未发现类似NGX和BGN融合的先例,因此我们的技术路线是一次探索,未必适合您的胃口;同时,我们的技术路线也未必符合主流NGX社区的思路,我们修改了NGX,但是又希望以最小的代价修改、最大的可能性扩展NGX,可能会伤到纯粹派的NGX拥护者。对此,我们深表歉意,但请您尊重我们的选择。 我们在LUA上做了多年的业务开发,使用了章亦春先生的NGX LUA模块。在NGX和BGN融合过程中,我们对NGX LUA中的部分接口做了使用上的限制、甚至修改、替换,由于这些变更并非纯粹针对NGX LUA的改良,更确切地说,是为了适配的目的,因此无法提交给社区,敬请章亦春先生谅解,同时,我们对NGX LUA的认识可能会引起章亦春先生及社区的不适,也一并请求谅解,我们所做的一切都是希望从业务、架构、性能角度来审视技术路线。 在NGX和BGN融合过程中,曾经出现巨大的障碍,我们无法跨过NGX LUA的协程调度引起的异常core dump。在这里我们要特别鸣谢今日头条的黄龙先生,通过对LuaJIT源码的分析,最终从原理上指引我们走上正确的解决之道。 # 2 名词解释 NGX:即nginx,本文中简写。 BGN:这是本文作者开发的一个分布式/并行计算平台,以任务的形式驱动,每个任务可以包含一个或多个子任务,每个子任务看作是一次远程函数调用,核心思想是分布式模块概念,任务在模块与模块之间、非模块向模块传递。目前维护的是协程版。NGX与BGN融合过程中,主要用到了BGN的协程池机制和BGN的基础框架,对任务以及私有通讯协议涉及很少。 # 3 融合的目的 NGX和BGN融合的目的是为了充分发挥NGX和BGN各自的优势、搭建一套便于开发的高性能基础服务平台。 从降低开发成本、特别是业务逻辑实现成本的角度来说,章亦春先生的NGX LUA模块已经完全解决了,为什么我们还要做NGX和BGN的融合呢? 答案就在于“高性能”,这是核心痛点。举例:NGX + LUA后,随便set一个变量,性能较NGX下降10倍。线上众多的高性能物理服务器,不能提供令人满意的性能指标。 分析业务逻辑、LUA流程和NGX LUA协程,可以看到LUA的部分瓶颈: 2 | (1) LUA是脚本语言,性能比原生NGX低 (2) 从NGX流程进入LUA脚本执行,整个“进入”过程长 (3) 变量的get/set操作,导致频繁查表、频繁在NGX和LUA之间穿梭 (4) 数据从NGX进LUA,以及从LUA进NGX,伴随着内存拷贝 (5) LUA脚本的灵活性和业务的复杂性,放纵了开发者,导致代码的执行效率低 (6) NGX LUA协程处于流程“中间”位置,导致暴露给LUA的接口存在性能上的缺陷 关于第(6)点举例,NGX LUA没有暴露epoll事件的接口,在处理网络IO时,不能做到被动式,即事件触发再去处理,只能通过timer事件轮询,这是性能低的重要原因之一。理论上,通过回调LUA以及修改NGX LUA应该可以做到,但代价太大,LUA业务逻辑要比现在复杂太多,可行性受到质疑。反观NGX和BGN融合,将整个局部流程,比如HTTP回源、与存储的交互、DNS解析等整体封装进BGN,对LUA暴露一个接口,调用接口时流程挂起,直到交互完成再返回的方式则简单、清晰很多。 不可否认,基于LUA的开发带来的便捷性,是NGX原生开发所不能比拟的,但对于CDN Cache这样的业务场景而言,性能损耗的累积带来的麻烦不容忽视。我们无意完全去掉LUA,但是有必要对LUA瘦身。在平衡开发效率和代码执行效率过程中,一个基本的思想是“微服务”:将流程控制交给LUA,将耗时的、逻辑不经常发生变化的通用部分交给C,并提供LUA API,供LUA脚本中调用。 微服务提供LUA API,最直接的方式就是在NGX LUA模块中实现,但这些微服务本身逻辑和LUA没有任何关系,从解耦的角度来说,不应该放在NGX LUA模块中。 微服务可以放在NGX中实现,以符合NGX模块理念的形式存在,但NGX的开发并不容易,诸多限制。微服务放在NGX中实现,不是最自由的选择。 那么微服务放在哪里实现呢? BGN具备实现微服务的全部技术基础,那么,把LUA作为胶水语言,在同一个nginx worker进程内粘合NGX和BGN,看起来是个不错的选择。 还有一个思路,将微服务彻底独立出来,成为一个真正的server,这需要将业务的逻辑一起打包带走,也是个不错的选择,由于增加通讯开销,性能上有所欠缺。 综合考虑,还是决定尝试NGX和BGN的融合。 ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/NGX%E4%B8%8EBGN%E8%9E%8D%E5%90%88/01-component.png) # 4 融合的技术路线 NGX和BGN分别是两套独立的系统、框架,放在同一个进程里,就需要有一个彼此都认同的东西来统一。我们的选择是:用协程统一。 原生的NGX是事件驱动模型,没有协程,章亦春先生的NGX LUA模块第一次引入协程,进而引入LUA脚本。 那么,为什么不直接动用NGX LUA模块中的协程技术呢?这主要是因为在NGX体系中所处的位置不同:NGX LUA中的协程(也称为LUA线程)是在HTTP请求中使用的,而我们要求的是整个NGX的运行由协程负责,也就是说,我们需要通过协程在顶层统一NGX和BGN。 为什么会选择协程的方式呢? 3 | 第一, BGN是协程方式,所有执行和调度都由协程处理,协程池采用master/slave模型; 第二, NGX是事件驱动,将事件的执行交给协程,是举手之劳,顺理成章; 第三, 在单个进程中,协程是理想的选择:无锁,就绪、执行、挂起、取消状态转移清晰可控,拥有独立的函数堆栈空间(依赖协程实现),无阻塞条件下执行效率高,并发度高。 NGX的事件主要分为三种: 4 | ## 4.1 epoll事件 特别地,我们考虑网络IO事件。NGX系统运转的初始动能来源于一次网络IO事件:HTTP请求的接入。 epoll_wait获取监听端口的读事件后,将该事件交给某个协程执行,完成请求的接入。 ## 4.2 timer事件 在HTTP处理过程中,经常有“过一段时间再看看”的需求,此时NGX流程抛出timer事件,超时后被触发。被触发时,Timer事件被提交给某个协程执行。 ## 4.3 post事件 在HTTP处理过程中,如果需要分心去做某件事,NGX流程就会抛出post事件,排队等待执行。在NGX主循环中,会依次执行队列中的post事件,此时将post事件加载到某个协程执行。 NGX是典型的“流式处理”,即整个HTTP的处理流程被切成一小块一小块的,称之为“split phase”(注:split phase和NGX的11个处理阶段没有半毛钱关系)。一个小块的处理过程中,会准备好下一个小块的handler,处理过程中有异常、有不同流程的选择,所以handler会非常丰富。当一个小块处理完成后,“流式”地进入下一个小块的处理过程。如果流程不能这么“任性”地“流”下去,怎么办?比如回源、访问外部资源、等待某个条件具备等。如果是等待外部资源就位,那好办,把下一个小块的处理挂在网络IO事件后即可;如果不需要网络交互、仅仅等待某个条件具备,NGX其实没有什么好手段,它只能通过timer事件轮询。 所以,NGX的策略就是尽可能将一切转化为网络IO事件,实在不行就轮询!每个小块的handler构成了一张纵横交错的逻辑网,怪复杂的。 这就是“流式处理”的毛病:复杂度太高。 NGX LUA很好地解决了这个问题:将异步回调转化为顺序调用的形式。那么,NGX LUA是如何做到在顺序执行的过程中,分身去做另一件事,然后回来继续顺序执行的呢?就是挂起!通过lua的yield切换出去,再通过resume切换回来。切换出去意味着当前流程被挂起,切换回来意味着该流程又可以恢复执行。 BGN从诞生第一天起就是为了实现这个目标:将异步转为为顺序,让并行与分布更自然。 异步转化为顺序,流程挂起,给开发人员卸下了沉重的负担。这是NGX LUA、BGN带来的红利,协程的挂起能力赋予了流程的挂起能力。 理想的方案应该是:一个HTTP请求的完整生命周期都发生在同一个协程中,通过流程的挂起完成整个请求的处理过程。可惜事实上做不到,NGX的流式处理,抛出事件的这种玩法,限制了理想的实现,我们需要向NGX妥协。 NGX和BGN作为独立的系统,分别有自己的主循环,我们将BGN的主循环嵌入到NGX主循环中,形成一个更大的、唯一的主循环。 NGX和BGN都可以做为独立的服务器,有自己的epoll机制,我们保持它们的独立性,承担正交的网络IO功能。 综上种种,用一句话来概括NGX和BGN融合的技术路线: 5 | 用协程在顶层统一NGX和BGN,形成双epoll的服务器,使NGX具备流程挂起能力。 # 5 融合的技术障碍 在NGX和BGN融合过程中,或者说我们用协程“改造”NGX的过程中,遇到过各种各样的障碍,根本的原因有两个:(1)NGX的流式处理不会考虑事件被调度时的中间态 (2)NGX LUA的协程干扰。 举例: 6 | 在NGX中,如果一个事件被执行,执行方式形如: ev->handler(ev) 一行代码搞定。 7 | 在融合环境下,事件的执行要分三步走: S1. 分配一个协程给handler S2. 协程排队,等待被调度执行 S3. 协程被调度,执行handler,即ev->handler(ev) 中间态在事件的状态迁移中必须被小心对待,留意时序的不确定性。幸运的是,NGX代码的实现相当优异,思路清晰,本身也是按照事件的“并行”处理设计的,因此解决起来问题不大。 对于NGX LUA的问题,就没那么幸运了:LUA协程(线程)的切换极有可能从一个BGN协程切到了另一个BGN协程,这是绝对不允许出现的。为什么呢? LUA协程和BGN协程实现不同: (1)LUA协程的堆栈严格意义上来说,它不会保存当前的函数堆栈,比如参数表、局部变量、当前指令位置等,它仅保存了一个函数返回地址(注:很像流式处理,对不对?),这可以从切出函数lua_yield的调用点看出来,总是发生在return处,如 8 | return lua_yield(L, 0); 这不是巧合,是LUA协程被设计成必须这么干: LUA协程的函数调用堆栈,形象地说,存的是回调,而不是类似C语言中的函数堆栈空间,这是LUA协程要支持不同平台和语言做出的抽象与牺牲。 而BGN的协程则保留着完整的函数调用堆栈信息,可以从任意点切出,切回来恢复继续往下执行。 (2)BGN的协程处于顶端,意味着每个LUA协程(线程)的运行,必然发生在某个BGN协程中(沙箱)。如果从一个LUA协程切换到另一个LUA协程时,是从一个BGN协程跳进了另一个BGN协程,就会导致BGN协程的函数调用堆栈被破坏,引起core dump。 好在章亦春先生的NGX LUA的LUA协程切换一般发生在同一个HTTP请求的多个LUA协程之间,这给NGX和BGN融合、同时兼容NGX LUA带来了一线生机。应该说,章亦春先生的NGX LUA在设计、组织和调度LUA协程时,是非常合理的。 我们要付出的代价是,改造NGX LUA,使LUA协程的切换限定在同一个BGN协程内;如果做不到,就重构NGX LUA接口;如果还做不到,对不起,那就禁止使用该NGX LUA接口。 代价相当惨烈。初期NGX和BGN融合后的不稳定因素,全部与NGX LUA有关。最安全的做法是,所有在LUA脚本中会发生异步行为的接口,统统换成BGN的接口,由BGN负责异步处理,LUA脚本中严格使用最基本的语法和接口,创建用户线程啦、异步socket通讯等高端玩法,全部废止,这可以从根上断掉LUA协程的调度。 这种削足适履的做法,是无奈的选择。理想的做法是:NGX和BGN直接交互,不带LUA玩。 您能想象NGX中HTTP请求和NGX LUA中的HTTP请求是貌合神离的吗? 根据配置,在HTTP请求处理的某个阶段,需要交给LUA脚本处理时,NGX LUA就会创建一个fake request。NGX LUA为了与NGX和谐相处,并没有改动过NGX代码和结构,严格遵循NGX模块开发理念,因此,我们可以从逻辑上审视request和fake request的关系: fake request可以找到request,反过来request并不知道fake request的存在。 这种单向的关系导致一个问题:当NGX走关断流程,释放request及connection时,NGX LUA并不知道,也即,不能及时撤销掉fake request及LUA协程。在NGX日志中会看到这样的现象:客户端已断链,LUA脚本还在执行,并试图向客户端发送数据。 这个问题对NGX LUA不是个事,因为在后面的LUA协程处理中,发现异常后就地卧倒,等NGX LUA调度器过来收尸。 但是在BGN协程接管NGX事件处理后,问题变得严重: 假如lua脚本中发起一个sleep(ngx.sleep),NGX LUA会创建一个LUA协程(线程)。在超时前,客户端断链,timer事件没有被取消;超时后,timer事件被BGN协程加载执行,访问request相关信息时,直接崩掉(此时NGX LUA还没有发现客户端断链,时序上timer事件被先执行了)。 咨询社区,得到如下回复, ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/NGX%E4%B8%8EBGN%E8%9E%8D%E5%90%88/02-community-response.png) 根据社区回复,在客户端断链后,相应的LUA协程不会被撤销,而是通过注册abort回调lua,也就是说回到LUA协程进行abort操作。 但是,我们的期望是不进LUA协程,而是站在BGN协程的角度来取消掉LUA协程,包括timer事件。于是我们在NGX LUA模块中加了一行清场的代码,也许不是最好的解决办法,但稳定了局面。 LUA堆栈则是一个不能彻底解决的障碍。 LUA的堆栈主要是用来在LUA和其它语言(这里是C语言)之间交换数据的。对于回源、与后端存储的交互,NGX拿到数据后,通过堆栈拷贝给LUA,发送数据时,又通过堆栈拷贝给NGX。业务逻辑处理过程中的LUA变量,同样会占用堆栈。 LUA堆栈大小固定,LUA自动回收机制GC效率较差,而主动调用GC回收,执行效率又太差。内存占用超过堆栈上限,引起LUA崩溃,业务就受到影响了。 控制LUA内存量,有严格控制变量的生命周期、显式将无用的变量置空、控制数据收发的流量等措施,但只能减缓,不能彻底、完美解决。还有一个取巧的办法:不定时reload一下,推倒重来。 # 6 合并回源 9 | NGX和BGN融合后,可以充分利用流程的挂起能力、BGN开发的便捷性,提供更复杂、更强大的功能。 合并回源就是一例。先提供流程示意图: ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/NGX%E4%B8%8EBGN%E8%9E%8D%E5%90%88/03-orig-merge.png) 几个名词解释: 10 | CLIENT:客户端,一般是普通网民 RFS:一种随机访问存储系统 ORIG:源站 特别提示: 11 | (1) 图中CLIENT、NGX、BGN、RFS、ORIG是五种角色,而不是五个实例。在系统运行中,每种角色对应的实例有多个,实例可能在同一个进程,也可能在不同进程;可能在同一台物理服务器,也可能在不同物理服务器上。 (2) 示意图并非回源流程的全貌,而是一个切图,也不涵盖全部的交互过程,比如异常流程等,只能保证示意完整。 (3) 在融合的NGX和BGN中,NGX和BGN两者之间并未直接交互,而是通过LUA实现交互的,因此图中的确是示意图。实际数据流如下: ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/NGX%E4%B8%8EBGN%E8%9E%8D%E5%90%88/04-data-flow.png) (4) 示意图中有红、蓝两个回源的流程,但经过合并后,只有蓝色流程在回源 来看看整个流程。 12 | 在某个相近的时间点上,两个客户端向NGX请求尚未缓存的资源,触发NGX的回源流程。 NGX选择合并回源流程,要求BGN遵照执行。 BGN向RFS发出对即将回源的资源上锁的指令,RFS对第一个到达的指令返回TOKEN,对后续到达的指令则不返回TOKEN。 BGN侧获得TOKEN的流程(蓝方)有权发起回源请求,在回源前,把自己登记到RFS,告诉RFS:我正在等待该资源,资源就位后记得通知我。 BGN侧未获得TOKEN的流程(红方),把自己登记到RFS,告诉RFS:我正在等待该资源,资源就位后记得通知我。 BGN侧蓝方回源,获取资源后,将资源存进RFS。RFS将资源存储后,通知所有正在等待该资源的各方,过来取资源。 BGN侧蓝方和红方分别去RFS取资源。 BGN侧蓝方获取资源后,向RFS侧发出解锁指令,然后向NGX侧返回合并回源结果。 BGN侧红方获取资源后,向NGX侧返回合并回源结果。 NGX向客户端返回资源。流程结束。 合并回源流程有如下看点: 13 | (1) 存储是最清楚资源是否已经缓存的,因此让存储参与到流程控制中是合理的。但是,如果存储不具备这种“参与”能力,怎么办?那就需要额外的角色来承担,如此一来,流程更长。 (2) 蓝方和红方在等待存储的资源就绪通知的过程中,流程被挂起,这是BGN协程赋予的能力。 (3) 存储在资源就绪时,通知等待各方,这是存储具备的反向通知能力。 (4) BGN接收并处理存储的资源就绪通知,这是融合后BGN的epoll赋予给NGX的能力:NGX的worker进程从“各向同性”被改造成“各向异性”,每个worker进程拥有独立的服务器(端口)。原生NGX从master进程继承全部服务器,worker进程无差异,请求打到任何一个worker都可以。BGN可以让每个worker进程具备独一无二的身份,从而支持反向通知处理能力。 如果保持NGX的worker进程的“各向同性”,能不能获得支持反向通知处理的能力呢?能!长连接!需要权衡为长连接付出的代价。 (5) 从BGN出发,所有与存储、源站的网络交互,走的是BGN的epoll。 从以上可以看到,BGN就是NGX的强大外挂。 # 7 融合后前景展望 为了融合NGX和BGN,我们对NGX的顶层架构进行了修改,这种修改是无害的、不会伤及NGX的处理流程、不会影响NGX版本升级;对NGX LUA的修改则是有害的,限制了LUA一些高级接口的使用,但所有LUA失去的接口功能,都可以从BGN中找回来,我们只需要克制欲望,使用基础的、无害的LUA接口即可。 目前,BGN的私有协议并未开启。私有协议的通讯效率要高于HTTP:私有协议可以在同一条连接上不断收发,没有请求-响应-请求-响应-请求这样的顺序要求,BGN内部自动匹配响应和请求。 BGN的任务驱动机制完善,提供比NGX的request和sub-request更强大的并行处理和分布式处理能力。 BGN有高效的内存块管理机制,功能在BGN实现,可以脱离NGX的生命周期结束再回收内存、以及LUA的GC机制,内存占用量控制很好,内存使用效率更高。 BGN有完善的(分布式)模块机制,完胜NGX的模块机制。 BGN的开发模式简单、成熟,基础设施完善,开发效率虽不如LUA,但远胜NGX,不涉及NGX原生流程的变动。 如果NGX和BGN直接交互,性能更好。 * 后记 14 | 15 | NGX与BGN直接交互已落地,形成了当前的[XCACHE](https://github.com/chaoyongzhou/XCACHE)架构。 -------------------------------------------------------------------------------- /XFS设计与实现/XFS介绍.md: -------------------------------------------------------------------------------- 1 | 1、背景 2 | 3 | XFS是一个面向小文件的高性能存储系统,文件带目录存储。 4 | 5 | 定义:在XFS中,小文件是指不超过64MB大小的文件。 6 | 7 | XFS按照单盘单进程管理,支持单盘的上下线操作。 8 | 9 | XFS是RFS(Random access File System)的延续,增加了裸盘管理和DMA分级缓存能力。 10 | 11 | 2、架构 12 | 13 | ![图 2-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-2-1.png) 14 | 15 | 16 | XFS对外提供三类接口: 17 | 18 | (1)REST API: HTTP协议,支持长连接和短连接通信 19 | 20 | (2)XFS API:私有协议,长连接通信 21 | 22 | (3)CONSOLE:私有协议,长连接通信 23 | 24 | 3、编译 25 | 26 | (1)编译XFS主可执行文件 27 | 28 | make -j xfs -f Makefile.xfs 29 | 30 | (2)编译XFS初始化工具可执行文件 31 | 32 | make -j xfs_tool -f Makefile.tool 33 | 34 | (3)编译CONSOLE可执行文件 35 | 36 | make console -f Makefile.console 37 | 38 | 4、运行 39 | 40 | XFS的运行依赖归一后的磁盘软链和配置文件。 41 | 42 | 其中,配置文件描述XFS进程对应的IP地址、服务端口、网络拓扑和配置参数,本文不表。 43 | 44 | 4.1、环境准备 45 | 46 | 创建磁盘软链,归一处理。举例: 47 | 48 | # ln -s /dev/sdf /data/cache/rnode1 49 | 50 | 4.2、初始化XFS元数据 51 | 52 | # bash /usr/local/xfs/bin/xfs_init.sh 53 | 54 | 4.3、启动XFS 55 | 56 | # /usr/local/xfs/bin/xfs -tcid 10.10.67.18 -node_type xfs -xfs_sata_path /data/cache/rnode1 -logp /data/proclog/log/xfs -sconfig /usr/local/xfs/bin/config.xml -d 57 | 58 | 启动命令完整格式为: 59 | 60 | # /usr/local/xfs/bin/xfs -tcid -node_type xfs -xfs_sata_path [-xfs_ssd_path ] -logp /data/proclog/log/xfs -sconfig /usr/local/xfs/bin/config.xml -d 61 | 62 | 即,ssd盘可选。 63 | 64 | 4.4、停止XFS 65 | 66 | # kill -15 67 | 68 | 5、接口 69 | 70 | XFS提供三种访问接口如下。 71 | 72 | 5.1、 REST API接口 73 | 74 | 举例: 75 | 76 | 写文件 77 | 78 | # curl -d "hello world" http://127.0.0.1:718/xfs/setsmf/top/level01/level02/level03/a.dat 79 | 80 | 读文件 81 | 82 | # curl -v http://127.0.0.1:718/xfs/getsmf/top/level01/level02/level03/a.dat 83 | 84 | 删文件 85 | 86 | # curl -v http://127.0.0.1:718/xfs/dsmf/top/level01/level02/level03/a.dat 87 | 88 | 删目录 89 | 90 | # curl -v http://127.0.0.1:718/xfs/ddir/top/level01 91 | 92 | 5.2、CONSOLE口 93 | 94 | 启动CONSOLE口 95 | 96 | # /usr/local/console/bin/console -tcid 0.0.0.64 -sconfig /usr/local/xfs/bin/config.xml 97 | 98 | 举例: 99 | 100 | 查看name node信息 101 | 102 | # hsxfs 0 show npp on tcid 10.10.67.18 at console 103 | 104 | 查看data node信息 105 | 106 | # hsxfs 0 show dn on tcid 10.10.67.18 at console 107 | 108 | 5.3、XFS API 109 | 110 | 举例: 111 | 112 | 写文件 113 | 114 | EC_BOOL cxfs_write(const UINT32 cxfs_md_id, const CSTRING *file_path, const CBYTES *cbytes) 115 | 116 | 更新文件(覆盖写) 117 | 118 | EC_BOOL cxfs_update(const UINT32 cxfs_md_id, const CSTRING *file_path, const CBYTES *cbytes) 119 | 120 | 读文件 121 | 122 | EC_BOOL cxfs_read(const UINT32 cxfs_md_id, const CSTRING *file_path, CBYTES *cbytes) 123 | 124 | 读部分文件 125 | 126 | EC_BOOL cxfs_read_e(const UINT32 cxfs_md_id, const CSTRING *file_path, UINT32 *offset, const UINT32 max_len, CBYTES *cbytes) 127 | 128 | 删文件 129 | 130 | EC_BOOL cxfs_delete_file(const UINT32 cxfs_md_id, const CSTRING *path) 131 | 132 | 删目录 133 | 134 | EC_BOOL cxfs_delete_dir(const UINT32 cxfs_md_id, const CSTRING *path) 135 | 136 | XFS API是XFS对外暴露接口的全集,截止目前共115个接口。 137 | 138 | 6、设计 139 | 140 | 6.1、存储模型 141 | 142 | 存储本质上由两部分组成:name node和data node,即要完成两次映射。 143 | Name node的作用就是从文件路径映射到文件内容的实际存储位置: 144 | 145 | Name node: path -------→ location 146 | 147 | 比如,读文件前,我们把文件的完整路径告诉给name node,name node检索后将结果返回,这样我们就知道文件存储在哪块磁盘、哪个物理文件(可选)、偏移量是多少、文件长度是多少等信息: 148 | 149 | (disk_no, file_no, offset, len)= NameNode(path) 150 | 151 | Data node用来管理磁盘空间的分配与回收。 比如,写文件时,我们把文件长度告诉给data node,data node经过一番折腾后返回,告诉我们在哪块磁盘、哪个物理文件(可选)、起始偏移量是多少的地方有所需要的空间: 152 | 153 | (disk_no, file_no, offset) = DataNode(file_size) 154 | 155 | 示意图: 156 | 157 | ![图 6-1-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-1-1.png) 158 | 159 | 160 | 6.2、Name node设计 161 | 162 | name node采用inode方式管理目录结构。 163 | 164 | 6.2.1、dnode/fnode 165 | 166 | XFS是带目录存储系统,在设计中,dnode用来表达目录层次结构,fnode用来记录文件位置信息、文件大小等,其中记录文件位置信息的就是inode,包含虚拟磁盘号(disk no),块号(block no)、页码(page no)等。 167 | 168 | dnode用红黑树(rb tree)组织,树根就是当前目录。由于子目录依然是红黑树,因此XFS的name node本质上是多层红黑树。 169 | 170 | 比如,XFS存储三个文件:/a/b/c, /a/d/e, /a/d/f,name node中组织形式表达为: 171 | 172 | ![图 6-2-1-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-2-1-1.png) 173 | 174 | 其中,{a, b, d}是一棵红黑树,{b, c}是一棵红黑树,{d, e, f}是一棵红黑树。 175 | 176 | 6.2.2、item/key 177 | 178 | XFS中文件或目录的路径(path),按照斜线(/)分割成path segment(又称key,以下用该称呼)。 179 | 180 | XFS以item的方式来组织dnode/fnode,且包含key信息。 181 | 182 | 当前,主流XFS版本的key按照63字节存储,item只记录key在key zone中的相对偏移量。 183 | 184 | 如果客户端请求文件的路径的key超过63字节,则通过MD5压缩到32字节。 185 | 186 | 在不影响理解的语境下,约定成俗,我们口头上不区分item/inode。比如说一个inode占64字节,实际是指一个item占64字节。 187 | 188 | ![图 6-2-2-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-2-2-1.png) 189 | 190 | 6.3、Data node设计 191 | 192 | data node负责磁盘空间的分配与回收。 193 | 194 | data node按照卷(volume)、虚拟磁盘(virtual disk)、块(block)、页(page)四级管理磁盘空间。其中,volume最大支持到64TB, virtual disk固定1TB(或其它,比如32GB),block固定64MB、page最小4KB(或其它,比如32KB)。 195 | 196 | data node可分配的最小单位为page,即4KB(或其它,比如32KB),最大单位为block,即64MB。 197 | 198 | ![图 6-3-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-3-1.png) 199 | 200 | 201 | data node空间管理核心算法是分裂与合并,是一对互逆操作。 202 | 203 | 6.4、分裂与合并 204 | 205 | 分裂原理一句话概括为:一分为二取其左,反复迭代。 206 | 207 | 合并原理一句话概括为:看奇偶,看空闲,左右合并,反复迭代。 208 | 209 | 分裂与合并与伙伴系统高度相似。 210 | 211 | 具体示例:假设最小页定义为4KB,现有一个128KB的空闲空间,用绿色块代表空闲的4KB页,红色块代表已被占用的4KB页。 212 | 213 | (1)分配1个页 214 | 215 | ![图 6-4-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-1.png) 216 | 217 | S1~S5为分裂步骤 218 | 219 | (2)再分配5个页 220 | 221 | ![图 6-4-2](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-2.png) 222 | 223 | S1~S2为分裂步骤 224 | 225 | (3)再分配9个页 226 | 227 | ![图 6-4-3](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-3.png) 228 | 229 | S1~S3为分裂步骤 230 | 231 | (4)再分配3个页 232 | 233 | ![图 6-4-4](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-4.png) 234 | 235 | S1为分裂步骤 236 | 237 | (5)再分配3个页 238 | 239 | ![图 6-4-5](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-5.png) 240 | 241 | S1为分裂步骤 242 | 243 | (6)释放第0页 244 | 245 | ![图 6-4-6](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-6.png) 246 | 247 | M1为合并终止点 248 | 249 | (7)再释放第28~30页 250 | 251 | ![图 6-4-7](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-7.png) 252 | 253 | M1为合并终止点 254 | 255 | (8)再释放第8~12页 256 | 257 | ![图 6-4-8](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-8.png) 258 | 259 | M1为合并终止点 260 | 261 | (9)再释放第4~6页 262 | 263 | ![图 6-4-9](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-4-9.png) 264 | 265 | M1为合并终止点 266 | 267 | 268 | 6.5、磁盘布局 269 | 270 | XFS管理的磁盘一般由元数据区和数据区构成。元数据主要是name node和data node,数据区是指存储文件内容的区域。 271 | 272 | 6.5.1、磁盘布局 273 | 274 | 根据位置的不同,磁盘布局有如下三种方式。不失一般性,以SATA盘示例。假设XFS虚拟磁盘大小为32GB。 275 | 276 | (1)元数据区在磁盘头部位置 277 | 278 | ![图 6-5-1-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-5-1-1.png) 279 | 280 | 281 | (2)元数据区在磁盘尾部位置 282 | 283 | ![图 6-5-1-2](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-5-1-2.png) 284 | 285 | (3)元数据区在磁盘之外 286 | 287 | SATA盘为纯数据盘,元数据放在SSD盘。 288 | 289 | ![图 6-5-1-3](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-5-1-3.png) 290 | 291 | XFS对以上三种布局都支持过,最新版本保留对第(2)、(3)两种方式的支持。 292 | 293 | 6.5.2、元数据区布局 294 | 295 | ![图 6-5-2-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/XFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/xfs-6-5-2-1.png) 296 | 297 | 其中,config用来引导XFS的启动,记录各区域偏移量和大小信息,包括但不限于: 298 | (1)SATA盘的容量、 299 | (2)数据区在SATA盘的起始偏移量 300 | (3)SATA盘XFS元数据大小 301 | (4)虚拟磁盘大小、虚拟磁盘个数 302 | (5)name node在SATA盘的偏移范围 303 | (6)data node在SATA盘的偏移范围 304 | (7)SSD盘的容量(如果开启分级缓存中的SSD缓存) 305 | 306 | 7、分级缓存 307 | 308 | 分级缓存(内存缓存、SSD缓存、SATA缓存)的效果,是将内存、SSD、SATA作为一个整体,暴露给XFS访问,即从XFS(或其它应用)视角看到的是一块SATA盘,通过DMA接口访问;而从DMA视角看到的是三级缓存,需要根据性能优先、读写优先的原则决定访问顺序,并根据一定的策略决定数据的升级与降级。 309 | 310 | XFS管理它所看到的“SATA盘”的元数据,而DMA则管理内存缓存和SSD缓存的元数据。内存缓存和SSD缓存本质上是一个小型的、简化版的XFS,即也是一个存储系统。 311 | 312 | 关于分级缓存细节,请参见相关文档。 -------------------------------------------------------------------------------- /RFS设计与实现/RFS技术原理与实现剖析.md: -------------------------------------------------------------------------------- 1 | # RFS技术原理与实现剖析 2 | 3 | 目录 4 | 5 | 1 背景 6 | 7 | 2 架构 8 | 9 | 2.1 网络图 10 | 11 | 2.2 接口图 12 | 13 | 2.3 交互图 14 | 15 | 3 接口 16 | 17 | 3.1 REST API接口 18 | 19 | 3.2 CONSOLE口 20 | 21 | 3.3 RFS API接口 22 | 23 | 4 name node原理与实现 24 | 25 | 4.1 dnode/fnode/inode 26 | 27 | 4.2 item/key 28 | 29 | 5 data node原理与实现 30 | 31 | 5.1 分裂与合并原理 32 | 33 | 5.2 分裂与合并实现 34 | 35 | 5.3 空间优化 36 | 37 | 38 | 39 | ## 1 背景 40 | 41 | RFS(Random access File System)是当年为了解决nginx用作CDN Cache软件时,没有相应的存储系统(Cache Storage),从而基于BGN平台开发的。 42 | 43 | RFS是BGN平台上的众多模块之一,其基础组件(比如网络通信、内存管理、日志系统、接口库等)依赖BGN。 44 | 45 | CDN行业存在一个行业的痛点:刷新。RFS在设计之初希望切除该痛点,因此选择带目录存储的架构,实现URL和目录的真实秒刷能力。 46 | 47 | 根据存储与业务分离原则,RFS不负责切片、不负责回源,仅负责读、写、删三个核心功能。在XCACHE设计时,由于合并回源功能的汇聚点选择落在存储上,因此RFS额外承担了其中的汇聚辅助功能。 48 | 49 | RFS面向块设备,但当时没有直接进行裸盘管理,其进化版本XFS提供了裸盘管理、AIO、分级缓存等能力。 50 | 51 | RFS是被重度设计的,元数据较大,这使得通过调整元数据大小、修改元数据组织形式,即可衍生出不同种类的存储,比如HFS、SFS等。 52 | 53 | ## 2 架构 54 | 55 | ### 2.1 网络图 56 | 57 | ![图 2-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-2-1.png) 58 | 59 | 注: NP - Name Space/ Name Node, DN - Data Node 60 | 61 | (1) share-nothing 62 | 63 | (2)scale-out 64 | 65 | (3)单盘单进程管理 66 | 67 | (4)负载均衡 68 | 69 | ### 2.2 接口图 70 | 71 | ![图 2-2](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-2-2.png) 72 | 73 | (1) 控制台访问(CONSOLE) 74 | 75 | (2)远端访问(RFS API/ REST API) 76 | 77 | (3)中继访问(RFS API) 78 | 79 | ### 2.3 交互图 80 | 81 | ![图 2-3](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-2-3.png) 82 | 83 | 客户端访问RFS时,RFS首先查询Name Node,获得location信息,然后访问Data Node,读写数据。Data Node与磁盘交互。 84 | 85 | ## 3 接口 86 | 87 | RFS提供三种访问接口 88 | 89 | ### 3.1 REST API接口 90 | 91 | 举例: 92 | 93 | 写文件: curl -d "hello world" http://127.0.0.1:718/rfs/setsmf/top/level01/level02/level03/a.dat 94 | 95 | 读文件: curl -v http://127.0.0.1:718/rfs/getsmf/top/level01/level02/level03/a.dat 96 | 97 | 删文件: curl -v http://127.0.0.1:718/rfs/dsmf/top/level01/level02/level03/a.dat 98 | 99 | 删目录: curl -v http://127.0.0.1:718/rfs/ddir/top/level01 100 | 101 | ### 3.2 CONSOLE口 102 | 103 | 启动CONSOLE口: ./console -tcid 0.0.0.64 104 | 105 | 举例: 106 | 107 | 查看name node信息:hsrfs 0 show npp on tcid 10.10.67.18 at console 108 | 109 | 查看data node信息:hsrfs 0 show dn on tcid 10.10.67.18 at console 110 | 111 | ### 3.3 RFS API接口: 112 | 113 | 即BGN接口,通过私有协议访问RFS接口。 114 | 115 | 举例: 116 | 117 | 写文件:EC_BOOL crfs_write(const UINT32 crfs_md_id, const CSTRING *file_path, const CBYTES *cbytes) 118 | 119 | 读文件:EC_BOOL crfs_read(const UINT32 crfs_md_id, const CSTRING *file_path, CBYTES *cbytes) 120 | 121 | 删文件:EC_BOOL crfs_delete_file(const UINT32 crfs_md_id, const CSTRING *path) 122 | 123 | 删目录:EC_BOOL crfs_delete_dir(const UINT32 crfs_md_id, const CSTRING *path) 124 | 125 | ## 4 name node原理与实现 126 | 127 | name node采用inode方式管理目录结构。 128 | 129 | ### 4.1 dnode/fnode/inode 130 | 131 | RFS是带目录存储系统,在设计中,dnode用来表达目录层次结构,fnode用来记录文件位置信息、文件大小等,其中记录文件位置信息的就是inode,包含虚拟磁盘号(disk no),块号(block no)、页码(page no)等。 132 | 133 | dnode用红黑树(rb tree)表达目录层次信息,树根就是当前目录,树叶就是该目录下的文件,树枝则是该目录下的子目录。由于子目录依然是红黑树,因此RFS的name node的组织形式,本质上是多层红黑树。 134 | 135 | 比如,RFS存储三个文件:/a/b, /a/c/d, /a/c/e,name node中组织形式表达为: 136 | 137 | ![图 4-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-4-1.png) 138 | 139 | 其中,{a, b, c}是一棵红黑树,{c, d, e}是一棵红黑树。 140 | 141 | 142 | ### 4.2 item/key 143 | 144 | RFS中文件或目录的路径(path),按照斜线(/)分割成path segment(又称key,以下用该称呼)。 145 | 146 | RFS以item的方式来组织dnode/fnode,且包含key信息。 147 | 148 | 按照Linux POSIX文件系统的路径标准,RFS的key长度应为255字节,那么item对齐的话,需要512字节。RFS在发展过程中,为了缩减item的大小,放弃了支持这一标准。 149 | 150 | 当前,主流RFS版本的key按照63字节存储。以前的版本中,key嵌入item,总共占128字节,后来分离,各占64字节,item只记录key在key zone中的相对偏移量。 151 | 152 | 如果客户端请求文件的路径的key超过63字节,则通过MD5压缩到32字节。 153 | 154 | 在不影响理解的语境下,约定成俗,我们口头上不区分item/inode。比如说一个inode占64字节,实际是指一个item占64字节。 155 | 156 | ![图 4-2](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-4-2.png) 157 | 158 | ## 5 data node原理与实现 159 | 160 | data node负责磁盘空间的分配与回收。 161 | 162 | data node按照卷(volume)、虚拟磁盘(virtual disk)、块(block)、页(page)四级管理磁盘空间。其中,volume最大支持到64TB, virtual disk固定1TB(或其它,比如32GB),block固定64MB、page最小4KB。 163 | 164 | data node可分配的最小单位为page,即4KB,最大单位为block,即64MB。 165 | 166 | ![图 5-0](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-5-0.png) 167 | 168 | 169 | data node空间管理核心算法是分裂与合并,是一对互逆操作。 170 | 171 | 172 | ### 5.1 分裂与合并原理 173 | 174 | 分裂原理一句话概括为:一分为二取其左,反复迭代。 175 | 176 | 合并原理一句话概括为:看奇偶,看空闲,左右合并,反复迭代。 177 | 178 | 以从64KB连续空间上分配4KB为例: 179 | 180 | ![图 5-1-1](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-5-1-1.png) 181 | 182 | 将64KB一分为二,右边32KB标记为空闲空间, 183 | 184 | ![图 5-1-2](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-5-1-2.png) 185 | 186 | 左边32KB再一分为二,右边16KB标记为空闲空间, 187 | 188 | ![图 5-1-3](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-5-1-3.png) 189 | 190 | 左边16KB再一分为二,右边8KB标记为空闲空间, 191 | 192 | ![图 5-1-4](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-5-1-4.png) 193 | 194 | 左边8KB再一分为二,右边4KB标记为空闲空间 195 | 196 | ![图 5-1-5](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-5-1-5.png) 197 | 198 | 左边4KB即为所分配的空间。 199 | 200 | ![图 5-1-6](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-5-1-6.png) 201 | 202 | 203 | 64K Model表示64KB空闲空间的集合,并依次编号为page 0, page 1, ....,上述过程可以描述为: 204 | 205 | (64K Model, page 0) => (32K Model, page 0) + (32K Model, page 1) 206 | 207 | (32K Model, page 0) => (16K Model, page 0) + (16K Model, page 1) 208 | 209 | (16K Model, page 0) => (8K Model, page 0) + (8K Model, page 1) 210 | 211 | (8K Model, page 0) => (4K Model, page 0) + (4K Model, page 1) 212 | 213 | 空间回收时触发合并: 214 | 215 | 如果回收(4K Model, page 0),则检查(4K Model, page 1)是否空闲,若是,则合并,即 216 | 217 | (4K Model, page 0) + (4K Model, page 1) => (8K Model, page 0) 218 | 219 | 重复该过程: 220 | 221 | 检查(8K Model, page 1)是否空闲,若是,则合并,即 222 | 223 | (8K Model, page 0) + (8K Model, page 1) => (16K Model, page 0) 224 | 225 | 检查(16K Model, page 1)是否空闲,若是,则合并,即 226 | 227 | (16K Model, page 0) + (16K Model, page 1) => (32K Model, page 0) 228 | 229 | 检查(32K Model, page 1)是否空闲,若是,则合并,即 230 | 231 | (32K Model, page 0) + (32K Model, page 1) => (64K Model, page 0) 232 | 233 | 注意,如果回收(4K Model, page 1),则应检查(4K Model, page 0)是否空闲,即要判断page no的奇偶性。 234 | 235 | 236 | ![图 5-1-7](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/RFS%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/rfs-5-1-7.png) 237 | 238 | RFS以64MB为最大可分配空间、4KB为最小可分配空间,因此分裂最深是从64MB到4KB,合并最深是从4KB到64MB。 239 | 240 | RFS所管理的磁盘空间起始偏移量是64MB的整数倍。 241 | 242 | 分裂与合并算法有如下特点: 243 | 244 | (1)空间偏移量规整 245 | 246 | 比如,4KB空间偏移量必定4KB对齐,8KB空间偏移量必定8KB对齐 247 | 248 | (2)分裂与合并算法互逆 249 | 250 | (3)分裂与合并算法有稳定的时间复杂度 251 | 252 | ### 5.2 分裂与合并实现 253 | 254 | 首先将磁盘空间按照volume、virtual disk、block划分。然后在block中建立64M Model,32M Model, .... ,4K Model的空闲页表,初始时仅有64M Model表中有一个表项,即一个64MB空闲页。 255 | 256 | 按Model建立空闲页表的做法,同样适用于virtual disk和volume。比如,block中如果有64MB空闲页,那么这个block就挂到对应的virtual disk的64M Model中。 257 | 258 | 来看看如何分配一个4KB的空间: 259 | 260 | 首先,按照4K Model,8K Model,...,64M Model的次序查volume的空闲页表,找到第一个virtual disk,比如是在64K Model中找到的,这就是说该virtual disk存在64KB空闲页,但不存在比64KB更小的空闲页。 261 | 262 | 然后,按照64K Model, 128K Model, ..., 64M Model的次序查virtual disk的空闲表,找到第一个block,比如是在64K Model中找到的。 263 | 264 | 然后,按照64K Model, 128K Model, ..., 64M Model的次序查block的空闲表,找到第一个page并摘除, 比如是在64K Model中找到的,进行分裂操作,分裂出来的空闲页挂到对应的Model表中,返回最终分配到的4KB空间。 265 | 266 | 还没完,继续调整一下: 267 | 268 | 检查64K Model的block的空闲表是否已空,如果非空,流程结束;如果空,则将该block从virtual disk的64K Model表中摘除; 269 | 270 | 检查64K Model的virtual disk的空闲表是否已空,如果非空,流程结束;如果空,则将该virtual disk从volume的64K Model表中摘除,流程结束。 271 | 272 | 回收4KB的空间是一个逆过程: 273 | 274 | 根据4KB页的奇偶性,检查对应的block的4K Model表中的相邻页是否空闲,如果不空闲,则将该4KB页挂入block的4K Model表中,流程结束;否则,将相邻页从block的4K Model表中摘除,合并为一个8KB页; 275 | 再根据8KB页的奇偶性,检查对应的block的8K Model表中的相邻页是否空闲,...,如此迭代,至多迭代到block的64M Model表就会终止。相应地,block、virutal disk作出Model表的调整。 276 | 277 | 从以上流程中可以看到,Model表在频繁执行增、删、查操作,效率是关键,技术手段有: 278 | 279 | (1)位操作:与、或、移位。比如Model的确认、Model表的切换,page no的切换等。 280 | 281 | (2)位图:在每个Model表中建立位图,每比特表示该Model下对应页的空闲与否,用空间换时间。 282 | 283 | (3)红黑树:Model表上的空闲页按照红黑树管理,页的插入与删除操作的时间复杂度稳定。 284 | 285 | ### 5.3 空间优化 286 | 287 | 假如将所有Model表的所有空闲页投影到一条直线上,那么一定不会出现投影重叠的现象,因为空闲页是分裂而来的。 288 | 289 | 由于空闲页至少是4KB,所以所有Model表的所有空闲页总数不会超过磁盘空间可以划分成4KB页的个数(磁盘空间 / 4KB)。 290 | 291 | 进一步,由于合并算法的存在,所以所有Model表的所有空闲页总数不会超过磁盘空间可以划分成4KB页的个数一半(磁盘空间 / 4KB / 2)。 292 | 293 | 也就是说,要管理所有Model表的所有空闲页,只需要准备至多(磁盘空间 / 4KB / 2)个RB NODE即可。 294 | -------------------------------------------------------------------------------- /XFS使用/XFS上手.md: -------------------------------------------------------------------------------- 1 | # 1、xfs环境准备 2 | 3 | xfs支持裸盘访问,也支持块文件存储访问。 4 | 5 | 注意:由于存储空间要保留持久化元数据的空间,因此以块文件模拟磁盘时,建议文件大小不低于300G,用truncate命令创建即可。更小的块文件也可以通过调整xfs的编译参数、配置参数支持,不属于本文描述范围。 6 | 7 | 本文以Debian系统为例,6块NVMe盘/dev/nvme{0..5}n1,我们将1~5号盘作为数据盘,0号盘作为系统盘使用。 8 | 9 | 10 | S1. 创建目录 11 | 12 | # mkdir -p /data/cache 13 | 14 | 15 | S2. 建立软链: 16 | 17 | # ln -s /dev/nvme1n1 /data/cache/rnode1 18 | # ln -s /dev/nvme2n1 /data/cache/rnode2 19 | # ln -s /dev/nvme3n1 /data/cache/rnode3 20 | # ln -s /dev/nvme4n1 /data/cache/rnode4 21 | # ln -s /dev/nvme5n1 /data/cache/rnode5 22 | 23 | 结果如下: 24 | 25 | # ls -l /data/cache/ 26 | total 0 27 | lrwxrwxrwx 1 root root 12 Mar 9 17:13 rnode1 -> /dev/nvme1n1 28 | lrwxrwxrwx 1 root root 12 Mar 9 17:13 rnode2 -> /dev/nvme2n1 29 | lrwxrwxrwx 1 root root 12 Mar 9 17:13 rnode3 -> /dev/nvme3n1 30 | lrwxrwxrwx 1 root root 12 Mar 9 17:13 rnode4 -> /dev/nvme4n1 31 | lrwxrwxrwx 1 root root 12 Mar 9 17:13 rnode5 -> /dev/nvme5n1 32 | 33 | # 2、安装 34 | 35 | ## 2.1、安装命令 36 | 37 | # dpkg -i xfs-5.8.0.9_amd64.deb 38 | 39 | 安装目录和文件有: 40 | 41 | 依赖包目录: /usr/local/xfs/depend 42 | 可执行文件、脚本、配置目录:/usr/local/xfs/bin 43 | 定时任务文件:/etc/cron.d/xfs 44 | 服务脚本文件:/etc/init.d/xfs 45 | systemctl服务脚本模板:/etc/systemd/system/xfs.deb.service 46 | 日志目录:/data/proclog/log/xfs 47 | 48 | ## 2.2、初始化 49 | 50 | 初始化脚本根据已创建的软链,自动在各盘上创建xfs存储系统、生成配置文件、生成systemctl服务脚本。 51 | 52 | 执行命令: 53 | 54 | # bash /usr/local/xfs/bin/xfs_init.sh | tee aa.log 55 | 56 | 配置文件: 57 | 58 | # ls -l /usr/local/xfs/bin/config.xml 59 | -rw-r--r-- 1 root root 49744 Mar 10 21:58 /usr/local/xfs/bin/config.xml 60 | 61 | 生成的systemctl服务脚本: 62 | 63 | # ls -l /etc/systemd/system/xfs* 64 | -rw-r--r-- 1 root root 355 Mar 9 17:18 /etc/systemd/system/xfs@rnode1.service 65 | -rw-r--r-- 1 root root 355 Mar 9 17:18 /etc/systemd/system/xfs@rnode2.service 66 | -rw-r--r-- 1 root root 355 Mar 9 17:18 /etc/systemd/system/xfs@rnode3.service 67 | -rw-r--r-- 1 root root 355 Mar 9 17:18 /etc/systemd/system/xfs@rnode4.service 68 | -rw-r--r-- 1 root root 355 Mar 9 17:18 /etc/systemd/system/xfs@rnode5.service 69 | 70 | 注:初始化自动生成的配置文件,仅适用于单机场景,不适用节点场景,后者需要采用修改配置或者下发配置的方式进行。 71 | 72 | # 3、启动 73 | 74 | xfs启停建议使用脚本/etc/init.d/xfs,用法如下: 75 | 76 | # /etc/init.d/xfs 77 | 78 | usage: /etc/init.d/xfs 79 | start [<1-12>|] # start xfs servers by scan data cache dir /data/cache or start specific xfs server with rnode id 80 | restart [<1-12>|] # restart xfs servers by scan data cache dir /data/cache or restart specific xfs server with rnode id 81 | retrieve [<1-12>|] # retrieve xfs servers by scan data cache dir /data/cache or retrieve specific xfs server with rnode id 82 | stop [<1-12>|] # stop xfs servers with xfs closing and shutdown or stop specific xfs server with rnode id 83 | status [<1-12>|] # some or all xfs server(s) status 84 | monitor [<1-12>|] # monitor some or all xfs server(s) status. notify ngx bgn if anyone down 85 | rotate [<1-12>|] # rotate log on some or all xfs 86 | recycle [<1-12>|] # recycle some or all xfs deleted space 87 | garbage [<1-12>|] # retire expired locked files of some or all xfs 88 | flush [<1-12>|] # flush some or all xfs to disk 89 | retire [<1-12>|] # retire aging files of some or all xfs 90 | breathe [<1-12>|] # memory breathing on some or all xfs 91 | actsyscfg [<1-12>|] # activate system configure on some or all xfs 92 | status_np [<1-12>|] # namenode status on some or all xfs 93 | status_dn [<1-12>|] # datanode status on some or all xfs 94 | e.g. 95 | service xfs start 96 | service xfs start 1 97 | service xfs start 1,3,5 98 | 99 | 比如启动全部xfs: 100 | 101 | # /etc/init.d/xfs start 102 | 103 | 检查xfs状态: 104 | 105 | # /etc/init.d/xfs status 106 | [2020-03-24 14:38:04] XFS 10.20.67.18 ... running 107 | [2020-03-24 14:38:04] XFS 10.20.67.19 ... running 108 | [2020-03-24 14:38:04] XFS 10.20.67.20 ... running 109 | [2020-03-24 14:38:05] XFS 10.20.67.21 ... running 110 | [2020-03-24 14:38:05] XFS 10.20.67.22 ... running 111 | 112 | 或者检查进程: 113 | 114 | # ps -ef | grep xfs 115 | root 840789 731965 0 15:26 pts/8 00:00:00 grep xfs 116 | root 2716328 1 1 Mar20 ? 01:43:39 /usr/local/xfs/bin/xfs -tcid 10.20.67.21 -node_type xfs -xfs_sata_path /data/cache/rnode4 -logp /data/proclog/log/xfs -sconfig /usr/local/xfs/bin/config.xml -d 117 | root 2716365 1 1 Mar20 ? 01:27:51 /usr/local/xfs/bin/xfs -tcid 10.20.67.22 -node_type xfs -xfs_sata_path /data/cache/rnode5 -logp /data/proclog/log/xfs -sconfig /usr/local/xfs/bin/config.xml -d 118 | root 2744072 1 1 Mar20 ? 01:27:49 /usr/local/xfs/bin/xfs -tcid 10.20.67.18 -node_type xfs -xfs_sata_path /data/cache/rnode1 -logp /data/proclog/log/xfs -sconfig /usr/local/xfs/bin/config.xml -d 119 | root 2744430 1 1 Mar20 ? 01:29:54 /usr/local/xfs/bin/xfs -tcid 10.20.67.19 -node_type xfs -xfs_sata_path /data/cache/rnode2 -logp /data/proclog/log/xfs -sconfig /usr/local/xfs/bin/config.xml -d 120 | root 2745175 1 1 Mar20 ? 01:21:49 /usr/local/xfs/bin/xfs -tcid 10.20.67.20 -node_type xfs -xfs_sata_path /data/cache/rnode3 -logp /data/proclog/log/xfs -sconfig /usr/local/xfs/bin/config.xml -d 121 | 122 | # 4、访问 123 | 124 | xfs支持HTTP REST API访问和CONSOLE口访问 125 | 126 | ## 4.1、HTTP REST API访问 127 | 128 | 端口请参见/usr/local/xfs/bin/config.xml中的bgn项。 129 | 130 | 常用接口列表: 131 | 132 | (1)写文件 133 | 134 | 接口: 135 | 136 | POST ?mod=xfs&op=setsmf HTTP/1.1 137 | 138 | 举例: 139 | 140 | curl -X POST -v "http://127.1:718/bucket01/test.dat?mod=xfs&op=setsmf" -d "hello world" 141 | 142 | 143 | (2)读文件 144 | 145 | 接口: 146 | 147 | GET ?mod=xfs&op=getsmf HTTP/1.1 148 | 149 | 举例: 150 | 151 | curl -X GET -v "http://127.1:718/bucket01/test.dat?mod=xfs&op=getsmf" 152 | 153 | (3)更新文件 154 | 155 | 接口: 156 | 157 | PUT ?mod=xfs&op=update HTTP/1.1 158 | 159 | 举例: 160 | 161 | curl -X PUT -v "http://127.1:718/bucket01/test.dat?mod=xfs&op=update" -d "welcome to china" 162 | 163 | (4)删除文件 164 | 165 | 接口: 166 | 167 | DELETE ?mod=xfs&op=dsmf HTTP/1.1 168 | 169 | 举例: 170 | 171 | curl -X DELETE -v "http://127.1:718/bucket01/test.dat?mod=xfs&op=dsmf" 172 | 173 | (5)删除目录 174 | 175 | 接口: 176 | 177 | DELETE ?mod=xfs&op=ddir HTTP/1.1 178 | 179 | 举例: 180 | 181 | curl -X DELETE -v "http://127.1:718/bucket01/?mod=xfs&op=ddir" 182 | 183 | 184 | 更多接口请参考cxfshttp.c文件。 185 | 186 | ## 4.2、console访问 187 | 188 | 常见访问指令: 189 | 190 | hsxfs read hsxfs read file on tcid at 191 | hsxfs write hsxfs write file with content on tcid at 192 | hsxfs delete hsxfs delete {file|dir|path} on tcid at 193 | hsxfs qfile hsxfs qfile on tcid at 194 | hsxfs qdir hsxfs qdir on tcid at 195 | hsxfs qlist hsxfs qlist {full | short | tree} [of np ] on tcid at 196 | hsxfs show hsxfs show npp [] on tcid at 197 | hsxfs show hsxfs show dn on tcid at 198 | 199 | 比如: 200 | 201 | # ./console -tcid 0.0.0.64 202 | ------------------------------------------------------------ 203 | | | 204 | | WELCOME TO BGN CONSOLE UTILITY | 205 | | | 206 | ------------------------------------------------------------ 207 | bgn> hsxfs 0 show npp on tcid 10.20.67.18 at console 208 | [2020-03-24 15:17:44.448][tid 837727][co 0x555ac1a64d18] [rank_10.20.67.18_0][SUCC] 209 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp mgr: 210 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp model : 9 211 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp hash algo id : 1 212 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp item max num : 33554430 213 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp max num : 1 214 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp np size : 4294967296 215 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp np start offset : 17039360 216 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp np end offset : 4312006656 217 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp np total size : 4294967296 218 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp 0x556ecacfee30: np 0, fsize 4294967296, del size 411249592, recycle size 53402811 219 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] cxfsnp 0x556ecacfee30: header: 220 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] np 0, model 9, hash algo 1, item max num 33554430, item used num 236172 221 | [2020-03-24 15:17:44.448][tid 2744072][co 0x556ecc150670] pool 7f4bc32000f0, node_max_num 33554430, node_used_num 236172, free_head 18737534, node_sizeof = 64 222 | 223 | # 5、日志 224 | 225 | 日志目录: /data/proclog/log/xfs 226 | 227 | 日志文件: rank\_\\_.log -------------------------------------------------------------------------------- /seastar/seastar DEMO - 网络连接处理.md: -------------------------------------------------------------------------------- 1 | # 2 网络连接处理 2 | 3 | 现在搭建一个面向TCP连接的网络服务器示例。 示例的原始版本来自: [tutorial](https://github.com/scylladb/seastar/blob/9c4a3389c89ce0ecc55809366c15516f2d46516e/doc/tutorial.md) 4 | 5 | 改造如下: 6 | 7 | 8 | #include "core/app-template.hh" 9 | #include "core/reactor.hh" 10 | #include "core/sleep.hh" 11 | #include "core/thread.hh" 12 | #include "core/gate.hh" 13 | #include "core/temporary_buffer.hh" 14 | #include "core/iostream-impl.hh" 15 | 16 | #include "util/log.hh" 17 | 18 | #include 19 | #include 20 | 21 | using namespace seastar; 22 | 23 | using namespace std::chrono_literals; 24 | 25 | 26 | seastar::future<> entrance(); 27 | 28 | seastar::logger demo_logger("demo"); 29 | 30 | int main(int argc, char** argv) { 31 | seastar::app_template app; 32 | 33 | try { 34 | app.run(argc, argv, entrance); 35 | } catch(...) { 36 | std::cerr << "Couldn't start application: " 37 | << std::current_exception() << "\n"; 38 | return 1; 39 | } 40 | return 0; 41 | } 42 | 43 | 44 | seastar::future<> service_connection_handle(seastar::connected_socket s, 45 | seastar::socket_address a) { 46 | auto out = s.output(); 47 | auto in = s.input(); 48 | return do_with(std::move(s), std::move(out), std::move(in), [] (auto& s, auto& out, auto& in) { 49 | return seastar::repeat([&out, &in] { 50 | return in.read().then([&out] (auto buf) { // 从连接中读取数据,并将所读数据返回客户端 51 | if (buf) { 52 | demo_logger.debug("service_connection_handle: recv: '{}'", buf.get()); 53 | return out.write(std::move(buf)).then([&out] { 54 | demo_logger.debug("service_connection_handle: write done"); 55 | 56 | demo_logger.debug("service_connection_handle: flush"); 57 | return out.flush(); 58 | }).then([] { 59 | demo_logger.debug("service_connection_handle: send done"); 60 | return seastar::stop_iteration::no; 61 | }); 62 | } else { 63 | demo_logger.debug("service_connection_handle: buf is null"); 64 | return seastar::make_ready_future( 65 | seastar::stop_iteration::yes); 66 | } 67 | }); 68 | }).then([&out] { 69 | return out.close(); 70 | }); 71 | }); 72 | } 73 | 74 | seastar::future<> service_loop() { 75 | seastar::listen_options lo; 76 | lo.reuse_address = true; 77 | return seastar::do_with(seastar::listen(seastar::make_ipv4_address({9090}), lo), [] (auto& listener) { // 建立网络监听,端口9090 78 | return seastar::keep_doing([&listener] () { 79 | demo_logger.debug("service_loop: keep_doing enter"); 80 | return listener.accept().then([] (seastar::connected_socket s, seastar::socket_address a) { // 当有新连接进来时,调用service_connection_handle处理 81 | demo_logger.debug("service_loop: accepted connection from {}", a); 82 | // Note we ignore, not return, the future returned by 83 | // service_connection_handle(), so we do not wait for one 84 | // connection to be handled before accepting the next one. 85 | service_connection_handle(std::move(s), std::move(a)); // 注意此语句前没有return,即不阻塞 86 | }); 87 | }); 88 | }); 89 | } 90 | 91 | seastar::future<> entrance() { 92 | demo_logger.set_level(log_level::debug); 93 | 94 | return seastar::parallel_for_each(boost::irange(0, seastar::smp::count), 95 | [] (unsigned c) { 96 | return seastar::smp::submit_to(c, service_loop); // 所有核上运行service_loop 97 | }); 98 | } 99 | 100 | 这个服务器的功能是,在9090端口建立服务器监听。如果有客户端新连接接入,则从连接中读取数据,并将数据原封不动发回给客户端。 101 | 102 | 来看实现。 103 | 104 | 首先,通过核间消息传递,要求所有核并行运行service\_loop。 105 | 106 | service\_loop阻塞在do\_with的lambda,lambda阻塞在keep\_doing的lambda,这是一个无限循环。无限循环中的lambda阻塞在listener accept位置,accept后的连接不阻塞,直接抛出任务去执行service\_connection\_handle。 107 | 108 | 根据seastar源码分析4.6 “network stack”,只有0核上真正执行了listen,即0核上监听。 109 | 110 | 开启四个窗口,执行同样的telnet命令: 111 | 112 | telnet 127.1 9090 113 | 114 | 运行结果: 115 | 116 | DEBUG 2018-04-08 15:30:23,421 [shard 0] demo - service_loop: keep_doing enter 117 | DEBUG 2018-04-08 15:30:23,422 [shard 1] demo - service_loop: keep_doing enter 118 | DEBUG 2018-04-08 15:30:23,422 [shard 2] demo - service_loop: keep_doing enter 119 | DEBUG 2018-04-08 15:30:23,422 [shard 3] demo - service_loop: keep_doing enter 120 | DEBUG 2018-04-08 15:30:28,316 [shard 0] demo - service_loop: accepted connection from 127.0.0.1:56826 121 | DEBUG 2018-04-08 15:30:28,316 [shard 0] demo - service_loop: keep_doing enter 122 | DEBUG 2018-04-08 15:30:31,149 [shard 1] demo - service_loop: accepted connection from 127.0.0.1:56828 123 | DEBUG 2018-04-08 15:30:31,149 [shard 1] demo - service_loop: keep_doing enter 124 | DEBUG 2018-04-08 15:30:40,956 [shard 2] demo - service_loop: accepted connection from 127.0.0.1:56830 125 | DEBUG 2018-04-08 15:30:40,957 [shard 2] demo - service_loop: keep_doing enter 126 | DEBUG 2018-04-08 15:30:45,949 [shard 3] demo - service_loop: accepted connection from 127.0.0.1:56832 127 | DEBUG 2018-04-08 15:30:45,949 [shard 3] demo - service_loop: keep_doing enter 128 | 129 | accept发生在四个不同的核上?为什么不是都在0核上呢? 130 | 131 | 其实,accept确实发生在0核上,根据posix\_server\_socket\_impl::accept()接口的实现,它做了一个Round Robbin的负载均衡器,在0核上accept一个新的连接后,马上通过核间消息,转给了别的核来处理连接。 132 | 133 | (注:怀疑posix\_server\_socket\_impl::accept()接口的实现有毛病,咋是递归的呢?) 134 | 135 | service\_connection\_handle是一个无限循环:阻塞读数据,然后发送数据。这意味着,在service\_connection\_handle的调用点,如果加上return阻塞,那么listener.accept().then()被阻塞,无法进入seastar::keep\_doing的下一轮循环, 136 | 也就是不能调用listener.accept()接入新的连接请求。事实真的如此吗? 137 | 138 | 实测表明:不是!由于posix\_server\_socket\_impl::accept()接口的奇葩实现,导致新的连接能被接入,但是由于listener.accept().then()被前面连接阻塞,无法调用service\_connection\_handle来处理!(目测seastar这个accept实现有毛病) 139 | 140 | 无论如何,service\_connection\_handle调用点不能加阻塞。 141 | 142 | 现在,改造一下service\_connection\_handle:根据客户端不同的输入,指挥不同的核来处理,即消息分发。 143 | 144 | 145 | seastar::future> 146 | service_request_dispose(output_stream&& out, temporary_buffer buf) { // 这里必须要两个&符号,不懂为什么,反正只有这样才能编译过:-( 147 | if (buf) { 148 | char *data = buf.get_write(); 149 | size_t len = buf.size(); 150 | 151 | /*trim tail spaces and LF*/ 152 | while(len && ::isspace(data[ len - 1 ])) { -- len; } 153 | 154 | demo_logger.debug("service_connection_handle: recv: '{}' [{}]", data, len); 155 | 156 | if(1 == len && 0 == ::memcmp("a", data, len)) { // 客户端输入a,则由核1打印输出 157 | demo_logger.debug("service_request_dispose: submit 'a' to core 1"); 158 | return seastar::smp::submit_to(1, []{ 159 | demo_logger.debug("service_request_dispose: recv 'a'"); 160 | return seastar::stop_iteration::no; 161 | }); 162 | } 163 | 164 | if(1 == len && 0 == ::memcmp("b", data, len)) { // 客户端输入b,则由核2打印输出 165 | demo_logger.debug("service_request_dispose: submit 'b' to core 2"); 166 | return seastar::smp::submit_to(2, []{ 167 | demo_logger.debug("service_request_dispose: recv 'b'"); 168 | return seastar::stop_iteration::no; 169 | }); 170 | } 171 | 172 | if(1 == len && 0 == ::memcmp("c", data, len)) { // 客户端输入c,则由核3打印输出 173 | demo_logger.debug("service_request_dispose: submit 'c' to core 3"); 174 | return seastar::smp::submit_to(3, []{ 175 | demo_logger.debug("service_request_dispose: recv 'c'"); 176 | return seastar::stop_iteration::no; 177 | }); 178 | } 179 | 180 | if(1 == len && 0 == ::memcmp("q", data, len)) { // 客户端输入q,则返回ready future,触发service_connection_handle中的out.close(),关断与客户端的连接 181 | return seastar::make_ready_future( 182 | seastar::stop_iteration::yes); 183 | } 184 | 185 | return out.write(std::move(buf)).then([&out] { 186 | demo_logger.debug("service_connection_handle: write done"); 187 | 188 | demo_logger.debug("service_connection_handle: flush"); 189 | return out.flush(); 190 | }).then([] { 191 | demo_logger.debug("service_connection_handle: send done"); 192 | return seastar::stop_iteration::no; 193 | }); 194 | } else { 195 | demo_logger.debug("service_connection_handle: buf is null"); 196 | return seastar::make_ready_future( 197 | seastar::stop_iteration::yes); 198 | } 199 | } 200 | 201 | 202 | seastar::future<> service_connection_handle(seastar::connected_socket s, 203 | seastar::socket_address a) { 204 | auto out = s.output(); 205 | auto in = s.input(); 206 | return do_with(std::move(s), std::move(out), std::move(in), [] (auto& s, auto& out, auto& in) { 207 | return seastar::repeat([&out, &in] { 208 | return in.read().then([&out] (auto buf) { 209 | return service_request_dispose(std::move(out), buf.share()); // 从客户端连接读取数据后,交给请求分发接口 210 | }); 211 | }).then([&out] { 212 | return out.close(); 213 | }); 214 | }); 215 | } 216 | 217 | 运行结果: 218 | 219 | 220 | DEBUG 2018-04-08 16:10:44,333 [shard 0] demo - service_loop: keep_doing enter 221 | DEBUG 2018-04-08 16:10:44,334 [shard 2] demo - service_loop: keep_doing enter 222 | DEBUG 2018-04-08 16:10:44,334 [shard 1] demo - service_loop: keep_doing enter 223 | DEBUG 2018-04-08 16:10:44,335 [shard 3] demo - service_loop: keep_doing enter 224 | DEBUG 2018-04-08 16:10:49,719 [shard 0] demo - service_loop: accepted connection from 127.0.0.1:56844 225 | DEBUG 2018-04-08 16:10:49,720 [shard 0] demo - service_loop: keep_doing enter 226 | DEBUG 2018-04-08 16:10:53,716 [shard 0] demo - service_connection_handle: recv: 'a 227 | ' [1] 228 | DEBUG 2018-04-08 16:10:53,716 [shard 0] demo - service_request_dispose: submit 'a' to core 1 229 | DEBUG 2018-04-08 16:10:53,716 [shard 1] demo - service_request_dispose: recv 'a' 230 | DEBUG 2018-04-08 16:10:58,611 [shard 0] demo - service_connection_handle: recv: 'b 231 | ' [1] 232 | DEBUG 2018-04-08 16:10:58,612 [shard 0] demo - service_request_dispose: submit 'b' to core 2 233 | DEBUG 2018-04-08 16:10:58,613 [shard 2] demo - service_request_dispose: recv 'b' 234 | DEBUG 2018-04-08 16:11:00,915 [shard 0] demo - service_connection_handle: recv: 'c 235 | ' [1] 236 | DEBUG 2018-04-08 16:11:00,915 [shard 0] demo - service_request_dispose: submit 'c' to core 3 237 | DEBUG 2018-04-08 16:11:00,915 [shard 3] demo - service_request_dispose: recv 'c' 238 | DEBUG 2018-04-08 16:11:02,818 [shard 0] demo - service_connection_handle: recv: 'q 239 | ' [1] 240 | 241 | 从核0分发到不同的核上。 242 | (注:还有一个未解问题,buff如何分发到不同核上。貌似这个temporary\_buffer不好使。) 243 | 244 | 245 | -------------------------------------------------------------------------------- /amd/amd introduction.md: -------------------------------------------------------------------------------- 1 | # 三级存储AMD技术介绍 2 | 3 | 1 背景 4 | 5 | 2 存储基本知识 6 | 7 | 2.1 四层缓存 8 | 9 | 2.2 page cache 10 | 11 | 2.3 direct io 12 | 13 | 2.4 mmap 14 | 15 | 2.5 数据丢失 16 | 17 | 3 AIO基础知识 18 | 19 | 3.1 glibc版本的aio 20 | 21 | 3.2 内核版本的aio 22 | 23 | 3.2.1 关于io_cancel 24 | 25 | 3.2.2 关于偏移量和长度 26 | 27 | 3.2.3 优缺点 28 | 29 | 4 AIO模块 30 | 31 | 4.1 性能数据 32 | 33 | 5 存储设计 34 | 35 | 6 内存缓存(Mem Cache) 36 | 37 | 7 SSD缓存(SSD Cache) 38 | 39 | 8 三级缓存 40 | 41 | 8.1 三级缓存模型 42 | 43 | 8.2 AMD设计 44 | 45 | 8.3 流控与重试 46 | 47 | 8.4 页面流转 48 | 49 | # 1 背景 50 | 51 | AMD = AIO + Mem Cache + SSD Cache 52 | 53 | AMD技术产生的背景主要有两点: 54 | 55 | (1) SATA盘的IO能力挖掘不够,阻塞式随机读写,只能达到磁盘性能的50%。 56 | 57 | (2) 分级缓存下,Page Cache技术的运用,按照SSD -> Page Cache -> SATA读写顺序进行,不符合性能优先、读写优先原则,需要调整到 Mem Cache -> SSD -> SATA读写顺序。 58 | 59 | # 2 存储基本知识 60 | 61 | ## 2.1 四层缓存 62 | 63 | 读(写)磁盘文件时,通常要经历四个缓存: 64 | 65 | (1) 设备层缓存(disk cache) 66 | 67 | (2) 内核层缓存(page cache) 68 | 69 | (3) GLIBC缓存(clib buffer) 70 | 71 | (4) 应用层缓存(application buffer) 72 | 73 | 以读文件为例: 74 | 75 | 如果应用层自己有缓存,那么应用层可以直接从自己的缓存读取,无需POSIX文件系统调用; 76 | 77 | 否则, 如果应用层使用的是f**接口(比如fread, fwrite等),则接口检查GLIBC的缓存是否命中;否则,系统调用,进入内核查看page cache是否命中;否则,落盘访问。 78 | 79 | 如果开启O_DIRECT,则可以跳过page cache,直接落盘访问。 80 | 81 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/2-1-1.png) 82 | 83 | 84 | 在服务器端,通常不考虑GLIBC缓存,而设备层缓存(disk cache)不受应用层和内核控制,也可以不考虑。磁盘的设备层缓存(disk cache)又称DRAM,缺省关闭。 85 | 86 | 因此,服务器端主要考虑应用层缓存(application buffer)和内核层缓存(page cache)两种。 87 | 88 | ## 2.2 page cache 89 | 90 | 内核缓存(page cache)又分为两种: 91 | 92 | 一种是buffer cache,用于裸盘访问,见free命令输出的buffers列; 93 | 94 | 另一种是page cache,用于非裸盘访问,见free命令输出的cached列。 95 | 96 | 97 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/2-2-1.png) 98 | 99 | 100 | ## 2.3 direct io 101 | 102 | 内核缓存(page cache)是可选项,通过文件描述符的O_DIRECT来控制开启与否: 103 | 104 | 当设置O_DIRECT标志时,系统调用跳过内核缓存(page cache),通过IO队列(IO queue)和设备驱动(driver)与磁盘交互,内核不缓存数据,俗称direct io。 105 | 106 | 当不设置O_DIRECT标志时,系统调用读写请求与内核缓存(page cache)交互,内核线程pdflush检测内核缓存(page cache)的脏页并刷新到磁盘,或者从磁盘加载到内核缓存(page cache)。 107 | 108 | 设置O_DIRECT标志的方法: 可以在打开文件(open)时,传入标志;或者通过fcntl设置。示例代码如下: 109 | 110 | 打开时设置: 111 | 112 | fd = open(file_name, O_DIRECT | O_RDWR, 0666); 113 | 114 | 或者设置已打开文件描述符属性: 115 | 116 | 117 | int flags; 118 | 119 | flags = fcntl(fd, F_GETFL); 120 | 121 | if(-1 == flags) 122 | { 123 | return (-1); 124 | } 125 | 126 | if(0 > fcntl(fd, F_SETFL, flags | O_DIRECT)) 127 | { 128 | return (-1); 129 | } 130 | 131 | 注:内核缓存(page cache)缺省开启。如需关闭,需要显式设立O_DIRECT标志。 132 | 133 | direct io的第一个坑是对齐。 134 | 135 | 在读写文件时,需要四个参数:文件描述符(fd),文件的偏移量(offset),读写长度(size),应用层内存(buffer)。 136 | 137 | direct io要求后三个参数:文件的偏移量(offset),读写长度(size),应用层内存(buffer),必须与磁盘扇区大小对齐,通常是512字节对齐。 138 | 139 | 这为应用层对文件的读写操作带来了不便,换言之,应用层对文件的任意偏移量的访问便捷性,要归功于page cache,后者对应用层屏蔽了对齐的限制。 140 | 141 | ## 2.4 mmap 142 | 143 | mmap本质是将内核空间映射到用户空间。比如direct io关闭时,通过mmap将 文件(page cache)映射到用户空间,以减少内核态与用户态之间的切换,应用层通过基址+偏移量的方式访问连续的虚拟内存,mmap将该访问映射到文件的访问,并自动落盘,即对mmap内存的操作等同于对文件的操作。 144 | 145 | 在direct io开启时,使用mmap映射的方式不具备一般性,是否需要满足direct io的对齐要求等,个人尚未实践经验,可行性存疑。 146 | 147 | ## 2.5 数据丢失 148 | 149 | 多级缓存的目标是数据的最终一致。读写数据在多级缓存之间存在丢失的问题,以及数据不一致的问题。 150 | 151 | 152 | - 考虑内核缓存(page cache) 153 | 154 | 应用层将数据交给内核缓存(page cache)后立即返回,认为写成功。而page cache并非立即交给驱动(driver)写盘,而是经过一段时间后,由内核线程pdflush持久化到磁盘上。 155 | 156 | 设备掉电: 假如在数据持久化到磁盘之前或之中,突然掉电,那么数据丢失; 157 | 158 | 进程退出: 假如在数据持久化到磁盘之前或之中,应用层进程意外退出,内核线程pdflush将数据持久化到磁盘,数据不丢失。 159 | 160 | 161 | - 考虑应用层缓存(application buffer) 162 | 163 | 这里姑且认为应用层缓存是内存缓存(mem cache)。 164 | 165 | 应用层将数据交给内存缓存(mem cache)后立即返回,认为写成功。 166 | 167 | 设备掉电: 在数据持久化到SSD缓存(SSD Cache)前,如果掉电,数据丢失; 168 | 169 | 进程退出: 在数据持久化到SSD缓存(SSD Cache)前,如果进程意外退出,数据丢失。 170 | 171 | 程序异常: 在数据持久化到SSD缓存(SSD Cache)前,如果程序异常,比如内存不足导致必须释放掉一部分缓存时,数据丢失,应竭力避免此类异常。 172 | 173 | 174 | - 考虑SSD缓存(SSD Cache) 175 | 176 | 数据降级持久化到SSD缓存(SSD Cache)后,且尚未降级持久化到SATA盘,如果SSD盘损坏,数据丢失或读到脏数据。 177 | 178 | 此时,应用层数据认为数据已持久化,在SSD缓存(SSD Cache)数据丢失后,从SATA盘加载,但SATA盘并没有接收来自SSD盘降级的数据,出现不一致。 179 | 180 | # 3 AIO基础知识 181 | 182 | AIO即异步IO,本文特指磁盘的异步IO。 183 | 184 | AIO的核心是读写磁盘时,不用同步等待读写完成,而是提交一个异步读写事件,在事件完成时反向通知应用来处理即可。这种反向通知的机制就是熟知的触发机制,比如信号机制、epoll机制等。 185 | 186 | Linux有两个版本的AIO:一个是glibc用线程模拟的aio版本,一个是Linux内核实现的aio版本(又称原生AIO)。 187 | 188 | ## 3.1 glibc版本的aio 189 | 190 | 该版本提供的接口通俗易懂,简单易用,支持direct io和非direct io,可以利用内核缓存(page cache)提高效率。但是由于多线程的缘故,真实的效率并不高,不适用高性能服务器。 191 | 192 | 接口列表如下: 193 | 194 | 195 | int aio_read(struct aiocb *aiocbp); /* 提交一个异步读 */ 196 | int aio_write(struct aiocb *aiocbp); /* 提交一个异步写 */ 197 | int aio_cancel(int fildes, struct aiocb *aiocbp); /* 取消一个异步请求(或基于一个fd的所有异步请求,aiocbp==NULL) */ 198 | int aio_error(const struct aiocb *aiocbp); /* 查看一个异步请求的状态(进行中EINPROGRESS?还是已经结束或出错?) */ 199 | ssize_t aio_return(struct aiocb *aiocbp); /* 查看一个异步请求的返回值(跟同步读写定义的一样) */ 200 | int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout); /* 阻塞等待请求完成 */ 201 | 202 | 其中,struct aiocb主要包含以下字段: 203 | 204 | int aio_fildes; /* 要被读写的fd */ 205 | void * aio_buf; /* 读写操作对应的内存buffer */ 206 | __off64_t aio_offset; /* 读写操作对应的文件偏移 */ 207 | size_t aio_nbytes; /* 需要读写的字节长度 */ 208 | int aio_reqprio; /* 请求的优先级 */ 209 | struct sigevent aio_sigevent; /* 异步事件,定义异步操作完成时的通知信号或回调函数 */ 210 | 211 | 实现原理: 212 | 213 | 214 | 1、异步请求被提交到request_queue中; 215 | 2、request_queue实际上是一个表结构,"行"是fd、"列"是具体的请求。也就是说,同一个fd的请求会被组织在一起; 216 | 3、异步请求有优先级概念,属于同一个fd的请求会按优先级排序,并且最终被按优先级顺序处理; 217 | 4、随着异步请求的提交,一些异步处理线程被动态创建。这些线程要做的事情就是从request_queue中取出请求,然后处理之; 218 | 5、为避免异步处理线程之间的竞争,同一个fd所对应的请求只由一个线程来处理; 219 | 6、异步处理线程同步地处理每一个请求,处理完成后在对应的aiocb中填充结果,然后触发可能的信号通知或回调函数(回调函数是需要创建新线程来调用的); 220 | 7、异步处理线程在完成某个fd的所有请求后,进入闲置状态; 221 | 8、异步处理线程在闲置状态时,如果request_queue中有新的fd加入,则重新投入工作,去处理这个新fd的请求(新fd和它上一次处理的fd可以不是同一个); 222 | 9、异步处理线程处于闲置状态一段时间后(没有新的请求),则会自动退出。等到再有新的请求时,再去动态创建; 223 | 224 | 此版本不做进一步介绍。 225 | 226 | ## 3.2 内核版本的aio 227 | 228 | 内核版本的aio仅支持direct io,使用的门槛比较高,执行效率也高。该版本的接口没有对应的glibc接口,因此需要采用系统调用的方式,比如: 229 | 230 | 231 | syscall(__NR_io_setup, nr_reqs, ctx) 232 | 233 | 234 | 接口列表如下: 235 | 236 | 237 | int io_setup(int maxevents, io_context_t *ctxp); /* 创建一个异步IO上下文(io_context_t是一个句柄) */ 238 | int io_destroy(io_context_t ctx); /* 销毁一个异步IO上下文(如果有正在进行的异步IO,取消并等待它们完成) */ 239 | long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp); /* 提交异步IO请求 */ 240 | long io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result); /* 取消一个异步IO请求 */ 241 | long io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout) /* 等待并获取异步IO请求的事件(也就是异步请求的处理结果) */ 242 | 243 | 其中,struct iocb主要包含以下字段: 244 | 245 | __u16 aio_lio_opcode; /* 请求类型(如:IOCB_CMD_PREAD=读、IOCB_CMD_PWRITE=写、等) */ 246 | __u32 aio_fildes; /* 要被操作的fd */ 247 | __u64 aio_buf; /* 读写操作对应的内存buffer */ 248 | __u64 aio_nbytes; /* 需要读写的字节长度 */ 249 | __s64 aio_offset; /* 读写操作对应的文件偏移 */ 250 | __u64 aio_data; /* 请求可携带的私有数据(在io_getevents时能够从io_event结果中取得) */ 251 | __u32 aio_flags; /* 可选IOCB_FLAG_RESFD标记,表示异步请求处理完成时使用eventfd进行通知 */ 252 | __u32 aio_resfd; /* 有IOCB_FLAG_RESFD标记时,接收通知的eventfd */ 253 | 254 | 其中,struct io\_event主要包含以下字段: 255 | 256 | __u64 data; /* 对应iocb的aio_data的值 */ 257 | __u64 obj; /* 指向对应iocb的指针 */ 258 | __s64 res; /* 对应IO请求的结果(>=0: 相当于对应的同步调用的返回值;<0: -errno) */ 259 | 260 | 261 | 这里高效和奇妙之处在于aio\_buf,这是用户态地址空间中分配的,而aio请求时交给内核设备驱动(driver)执行,也就是说,内核直接访问用户空间地址。 262 | 263 | 也正因为此,内核版本的aio要求传入的参数aio\_buf(应用层内存),aio\_nbytes(文件读写长度), aio\_offset(文件偏移量)必须满足设备驱动(driver)的对齐要求。 264 | 265 | 用于反向通知的字段是aio\_resfd。内核版aio的实现机制是,如果aio请求指定了事件通知文件描述符(aio\_resfd),那么在读写完成后,内核向该文件描述符指向的文件中写入一个整数,表示有多少个读写事件完成。 266 | 267 | 这个描述符可以通过eventfd2或eventfd生成一个,然后放到epoll中查询读事件,当内核向该描述符中写入后,epoll读事件被触发。 268 | 269 | 注意,通过这个事件通知文件描述符,我们只知道有无读写事件完成、有多少个读写事件完成,但是具体并不知道是哪些已经提交的读写事件完成了。这个时候需要由系统调用接口io\_getevents查询,以获得读写完成的事件列表。 270 | 271 | 归纳内核版本aio的使用流程如下: 272 | 273 | 274 | 1、创建aio事件通知文件描述符,设置其读事件,挂入epoll中 275 | 2、创建aio上下文(io_setup),并记录下来 276 | 3、创建aio读写事件请求,通过已创建的aio上下文提交给内核(io_submit) 277 | 4、内核设备驱动(driver)读写完成,内核向aio事件通知文件描述符写入完成的事件数 278 | 5、aio事件通知文件描述符的epoll读事件触发,查询已完成的aio事件列表(io_getevents) 279 | 6、对已完成的aio事件逐一处理(典型的处理比如通知应用层读写完成) 280 | 281 | ### 3.2.1 关于io\_cancel 282 | 283 | 该系统调用取消一个aio请求。 284 | 285 | aio请求交给内核后进入IO队列(io queue),随后交给设备驱动(driver)执行。事实上,aio请求在交给设备驱动(driver)后,不能被取消,因此在实践中,应用层发起aio请求后,被成功取消的几率基本为零。因此,该系统调用的实际意义并不大,对于应用层aio请求取消的需求,其设计与实现不应依赖该系统调用。 286 | 287 | ### 3.2.2 关于偏移量和长度 288 | 289 | 内核版本aio请求中的偏移量和长度要求按扇区大小(512字节)对齐。为了描述的便利,不失一般性,假定应用层定义的页大小为1KB,即应用层发出的aio请求中的偏移量和长度按照1KB对齐,自然也满足设备驱动(driver)的对齐要求。 290 | 291 | 假定应用层需要访问的数据的偏移量是1000(单位字节),长度为2000(单位字节),也就是访问区间[1000, 3000),按页边界划分成三个区间:[1000, 1024),[1024, 2048),[2048, 3000),如下图所示。 292 | 293 | 也就是,第一页的[1000, 1024),第二页的[0, 1024),第三页的[0,952),这里数字代表页内偏移。 294 | 295 | 296 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/3-2-1.png) 297 | 298 | 299 | 如果该访问为读,那么应用层要发出三个aio请求,分别是读第一、二、三页:(page 1, read),(page2, read),(page 3, read)。aio读写完成后,应用层截取每页对应的区间即可。 300 | 301 | 如果该访问为写,那么应用层要发出五个aio请求,分别是读第一页,写第一页,写第二页,读第三页,写第三页:(page 1, read),(page 1, write)(page2, write),(page 3, read),(page 3,write)。 302 | 303 | 以第一页为例,应用层首先读取完整页内容,然后将待写数据覆盖掉相应的区间,再将更新过的页写入存储。先读后写严格序,不可错乱。第三页类似。 也就是,非对齐写时需要预读。 304 | 305 | 可见,非对齐写访问,会带来一到两次额外的aio请求,严格序还会带来访问延迟。 306 | 307 | ### 3.2.3 优缺点 308 | 309 | 优点:高效,零拷贝 310 | 311 | 缺点:技术复杂,门槛很高,应用层要做好适配,满足对齐和异步要求 312 | 313 | 应用层的适配可以由内存缓存(Mem Cache)完成,可视为用户空间版本的Page Cache。 314 | 315 | 316 | # 4 AIO模块 317 | 318 | AIO模块是指为了使用和适配原生AIO(即Linux内核版本aio)的功能集,包括但不限于 319 | 320 | 321 | (1)映射应用层随机读写请求到原生aio请求 322 | 323 | 参见3.2.2 关于偏移量和长度 324 | 325 | (2)解决对齐问题 326 | 327 | 参见3.2.2 关于偏移量和长度 328 | 329 | (3)解决读写竞争问题 330 | 331 | 读写竞争问题涵盖读写顺序问题和读写并发问题: 332 | 333 | 第一种是对同一个应用层访问,由于边界不对齐,导致同时需要读和写两类aio请求,以及严格序要求带来的读写竞争问题,参见3.2.2。 334 | 335 | 第二种是对多个并发的应用层访问同一个页带来的读写竞争,有的要求读,有的要求写,如下图: 336 | 337 | 338 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/4-1-1.png) 339 | 340 | 341 | R代表对此页的读操作,W代表对此页的写操作。 342 | 343 | 首先,访问同一个页的操作要严格按照FIFO的原则排队,对上层承诺可靠的时序。 344 | 345 | 其次,严格遵循基本读写序关系:在前序R未完成前,后续R和W不可执行,保证数据一致性。 346 | 347 | (4)解决读写合并问题 348 | 349 | 读写合并可以由AIO模块或者内存缓存(Mem Cache)负责,或者两者同时负责。 350 | 351 | 以上图为例,对R和W操作编号: 352 | 353 | 354 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/4-1-2.png) 355 | 356 | 357 | 先声明:这里Page是应用层内存,对应到存储的相同大小的区间,并最终和该区间同步。意味着所有该page上的操作,先在内存操作该page,然后发起aio请求,同步到存储。 358 | 359 | 在R1完成前, R2 ~ W6必须等待。 在R1完成后,R2 ~ W6可以依序立即执行,不用发起任何aio请求,在内存完成读写合并,因为R1与存储中的数据完全一致。 360 | 361 | 再看下图: 362 | 363 | 364 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/4-1-3.png) 365 | 366 | 在W1的aio请求发出前,R2 ~ W6可以依序立即执行,在内存完成读写合并,然后W1发起aio请求,因为W1与存储中的数据将最终一致。 367 | 368 | ## 4.1 性能数据 369 | 370 | 以256KB页面大小为例。AIO模块按256KB页为单位读写磁盘,应用层随机读写文件的偏移量和长度,被AIO模块处理、映射。设备为8 x 8T SATA,网络吞吐稳定在2.5Gbps ~ 1.7Gbps。 371 | 372 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/4-1-4.png) 373 | 374 | 375 | 下表为工具测试某厂商SATA盘的结果,视为磁盘吞吐极限: 376 | 377 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/4-1-5.png) 378 | 379 | 380 | 对比可以看到,AIO模块达到了磁盘吞吐极限。 381 | 382 | 383 | 384 | # 5 存储设计 385 | 386 | 所有的存储,本质上由两部分组成:name node和data node。 387 | 388 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/5-1-1.png) 389 | 390 | 391 | Name node的作用就是从文件路径映射到文件内容的实际存储位置: 392 | 393 | Name node: path -------→ location 394 | 395 | 比如,读文件前,我们把文件的完整路径告诉给name node,name node检索后将结果返回,这样我们就知道文件存储在哪块磁盘、哪个物理文件(可选)、偏移量是多少、文件长度是多少等信息: 396 | 397 | (disk_no, file_no, offset, len)= NameNode(path) 398 | 399 | 那么data node的作用又是什么呢?data node就是用来管理磁盘空间的。 比如,写文件时,我们把文件长度告诉给data node,data node经过一番折腾后返回,告诉我们在哪块磁盘、哪个物理文件(可选)、起始偏移量是多少的地方有所需要的空间: 400 | 401 | (disk_no, file_no, offset) = DataNode(file_size) 402 | 403 | 不同的Name Node和Data Node的设计,形成不同的存储系统。 404 | 405 | 比如,如果Name Node和Data Node都在内存中,无需持久化到磁盘,那么就是内存缓存(Mem Cache)。 406 | 407 | 又比如,如果文件路径是某个特定的key,比如表示一段存储空间的区间 [存储介质序号,开始页,结束页),那么这个存储就可以作为另一个存储的缓存,比如内存缓存(Mem Cache)、SSD缓存(SSD Cache)。 408 | 409 | 以三级缓存为例: 410 | 411 | SATA盘持久化数据,SSD缓存(SSD Cache)作为二级缓存, 内存缓存(Mem Cache)作为一级缓存。 412 | 413 | 读文件时,先检查SATA盘的Name Node确认文件存在,再检查内存缓存(Mem Cache)是否命中;若否,再检查SSD缓存(SSD Cache)是否命中;若否,读SATA盘。 414 | 415 | 写文件时的数据流方案有多个,这里举一个例子:先写入内存缓存(Mem Cache),然后内存缓存(Mem Cache)的LRU机制和降级机制联动,降级到SSD缓存(SSD Cache),然后SSD缓存(SSD Cache)的LRU机制和降级机制联动,降级到SATA。 416 | 417 | SATA盘以文件路径(path)为键值,文件的位置信息用[磁盘号,开始页,结束页)标识。内存缓存(Mem Cache)和SSD缓存(SSD Cache)以[磁盘号,开始页,结束页)为键值。通过键值的变化,联系三级缓存。 418 | 419 | 以上三级缓存只是一种设计方式,供读者参考。 420 | 421 | # 6 内存缓存(Mem Cache) 422 | 423 | 内存缓存(Mem Cache)特指在用户空间开辟一块连续的内存空间,存储用户数据。应用层读写数据时,首先与内存缓存交互,避免用户态和内核态切换、与磁盘交互带来的消耗,提高性能。 424 | 425 | 广义讲,内核缓存(page cache)也是一种内存缓存,我们不考虑内存缓存(Mem Cache)和内核缓存(page cache)同时存在的情形,这有点重复和多余。 426 | 427 | 在direct io开启时,内存缓存(Mem Cache)的意义尤为明显,变相地充当了内核缓存(page cache)的功效,只是不用切换到内核态。 428 | 429 | 内存缓存(Mem Cache)根据业务的特点,可以简化为对特定大小页(比如,256KB,512KB等 )的存储。特化带来的好处有两个:简单高效、满足AIO对齐要求。 430 | 431 | 内存缓存(Mem Cache)所使用的内存空间有限,在持续缓存至给定的内存空间耗尽时,需要根据LRU算法淘汰当前最冷的数据至下一级缓存,比如SSD缓存(SSD Cache)。 432 | 433 | 设计请参照第4节。 434 | 435 | 436 | # 7 SSD缓存(SSD Cache) 437 | 438 | SSD缓存(SSD Cache)指在SSD盘上开辟一块空间(或者整张盘),存储用户数据。数据的来源通常是内存缓存(Mem Cache)或者SATA盘。 439 | 440 | SSD缓存(SSD Cache)主要是利用SSD的高IOPS能力、高读写吞吐(特别是读吞吐)能力,缓存热点数据,减少用户空间文件读写请求打到SATA盘上,从而提高存储性能。 441 | 442 | 相较于内存缓存(Mem Cache),SSD缓存(SSD Cache)具有如下不同点: 443 | 444 | (1)更大的存储空间 445 | 446 | 不言而喻。 447 | 448 | (2)数据持久化 449 | 450 | 内存缓存(Mem Cache)的数据是易失的, 而SSD缓存(SSD Cache)可以持久化数据。 451 | 452 | SSD缓存(SSD Cache)根据业务的特点,可以简化为对特定大小页(比如,256KB,512KB等 )的存储,有利于direct io。 453 | 454 | SSD缓存(SSD Cache)所使用的磁盘空间有限,在持续缓存至给定的磁盘空间耗尽时,需要根据LRU算法淘汰当前最冷的数据。 455 | 456 | 设计请参照第4节。 457 | 458 | SSD缓存(SSD Cache)可以考虑使用direct io + aio。需要指出的是,对于direct io,只需要满足对齐要求即可,对于aio,由于异步的存在,会导致分级存储的联动复杂度上升。 459 | 460 | # 8 三级缓存 461 | 462 | ## 8.1 三级缓存模型 463 | 464 | 三级缓存模型即Mem Cache + SSD Cache + SATA构成的缓存系统,根据数据的冷热程度缓存到不同的介质上,如下图所示。 465 | 466 | 至下而上,IOPS能力和吞吐能力逐步升高、数据热度逐步升高、存储容量逐步降低。 467 | 468 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/8-1-1.png) 469 | 470 | 471 | ## 8.2 AMD设计 472 | 473 | 对于设备配置,举例:8 x 12T SATA + 4 x 960G SSD,将SSD盘一分为二,每个分区480G,对应到一块SATA盘。数据采用严格降级方式缓存。 474 | 475 | 写数据时,首先写入内存缓存,然后降级到SSD盘,然后降级到SATA盘; 476 | 477 | 读数据时,首先检查内存缓存(Mem Cache),如果未命中,检查SSD缓存(SSD Cache),如果命中,加载至内存缓存(Mem Cache);如果未命中,从SATA盘加载至内存缓存(Mem Cache)。 478 | 479 | 480 | 每个线程(或进程)创建一个AMD模块实例,对外提供交互接口,对内协调内存缓存(Mem Cache)、 SSD缓存(SSD Cache)和SATA访问,进行流程控制。 481 | 482 | 483 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/8-2-1.png) 484 | 485 | 486 | 487 | POSIX文件读写接口关注四个参数:文件描述符、文件偏移量、文件长度、文件内容(或buffer,不用特别区分)。 488 | 489 | 考虑对同一文件的操作,因此只考虑后面三个参变量,读写接口可以表达为:POSIX(偏移量, 长度, 内容)。 490 | 491 | 又考虑到(偏移量,长度)与(起始偏移量,结束偏移量)完全等价,因此读写接口可以表达为:POSIX(起始偏移量,结束偏移量,内容)。 492 | 493 | AMD为了对上层应用提供透明接口,读写接口同样表达为:AMD(起始偏移量,结束偏移量,内容)。 494 | 495 | AMD中的AIO、内存缓存(Mem Cache)和SSD缓存(SSD Cache)也同样表达:AIO(起始偏移量,结束偏移量,内容),Mem(起始偏移量,结束偏移量,内容), SSD(起始偏移量,结束偏移量,内容)。 496 | 497 | 换言之,AMD内外与POSIX接口保持一致。这也意味着,AIO,内存缓存(Mem Cache)和SSD缓存(SSD Cache)与POSIX兼容,且具备独立部署能力。 498 | 499 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/8-2-2.png) 500 | 501 | 502 | ## 8.3 流控与重试 503 | 504 | 在三级缓存体系中,SATA盘的IO能力最弱。数据从SSD降级存储到SATA盘(对应SATA盘写操作)时, 很容易打爆SATA,因此需要进行流控处理,控制从SSD盘到SATA盘的数据流速。 505 | 506 | 反过来,SSD到SATA盘的流控,又限制了内存缓存(Mem Cache)的数据流入速度,即不能超过流控标定的速度,换言之,SATA盘的写入能力,决定了三级缓存整体的写入能力。 507 | 508 | 考虑到内存缓存(Mem Cache)容量最小且易失,内存缓存(Mem Cache)向SSD缓存(SSD Cache)降级存储过程中发出的AIO请求,容易撑爆AIO队列,引发超时或内存耗尽等异常。 509 | 510 | 作为重试(确保内存数据能够持久化到磁盘)的手段之一,可以考虑向SSD盘或者SATA盘,直接发起DIO(direct io)请求,即通过阻塞的方式延长读写请求处理时间,确保数据能够持久化到磁盘,同时也是一种流控手段。这就是AIO降级到DIO策略。特别警告:该策略考验对严格时序的控制能力,极难控制,强烈建议不要采用,变相地可以用重试策略替代。 511 | 512 | 数据的存储降级要持续进行,不能等到缓存已满或将满时进行,否则来不及处理。可以采用化整为零的方式持续进行:结合LRU,周期性地将当前缓存中的一部分最冷数据,降级到下一级缓存。这是降级存储机制。 513 | 514 | 当前缓存中的数据,如果已经被降级存储,即变为可丢弃数据。可以在缓存已满时,或者周期性地淘汰可丢弃数据。这是淘汰机制。 515 | 516 | 降级存储机制和淘汰机制都建立在LRU之上,结合流控来看,降级和淘汰应协调一致,确保三级缓存运行流畅。 517 | 518 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/8-3-1.png) 519 | 520 | 521 | ## 8.4 页面流转 522 | 523 | 内存缓存(Mem Cache)和SSD缓存(SSD Cache)各自维护一个页面(page)的LRU链表,页面从表头插入,表尾淘汰。 524 | 525 | SATA也维护一个类似的LRU链表,但不受AMD控制。 526 | 527 | 内存缓存(Mem Cache)的页面从LRU表尾淘汰前,由降级机制刷到SSD缓存(SSD Cache)。 528 | 529 | SSD缓存(SSD Cache)的页面从LRU表尾淘汰前,由降级机制刷到SATA。 530 | 531 | 访问的页面在SSD缓存(SSD Cache)或者SATA命中时,升级到内存缓存(Mem Cache)。 532 | 533 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/amd/8-4-1.png) 534 | 535 | -------------------------------------------------------------------------------- /aeron/Aeron浅析.md: -------------------------------------------------------------------------------- 1 | 2 | 目录 3 | 4 | 1 引言 5 | 6 | 2 基础知识:UDP通讯 7 | 8 | 2.1 通讯五元组 9 | 10 | 2.2 UDP单播 11 | 12 | 2.3 UDP组播 13 | 14 | 2.4 UDP广播 15 | 16 | 2.5 Aeron中的单播 17 | 18 | 2.6 Aeron中的组播 19 | 20 | 2.7 Aeron中的广播 21 | 22 | 2.8 Aeron中的组播地址约定 23 | 24 | 3 基础知识:共享内存通讯 25 | 26 | 3.1 命名共享内存 27 | 28 | 3.2 共享内存目录 29 | 30 | 3.3 原子操作 31 | 32 | 4 基础知识:多线程通讯 33 | 34 | 4.1 多线程通讯 35 | 36 | 4.2 aeron中的多线程 37 | 38 | 5 Design Overview 39 | 40 | 5.1 Media 41 | 42 | 5.2 Media Driver 43 | 44 | 5.3 Client 45 | 46 | 6 Ring Buffer 47 | 48 | 7 Log Buffer 49 | 50 | 8 Frame 51 | 52 | 8.1 Setup Frame 53 | 54 | 8.2 Status Frame 55 | 56 | 8.3 Data Frame 57 | 58 | 8.4 PAD Frame 59 | 60 | 8.5 Heartbeat Frame 61 | 62 | 8.6 NAK Frame 63 | 64 | 8.7 RTT Frame 65 | 66 | 8.8 Error Frame 67 | 68 | 9 Term 69 | 70 | 10 Fragment 71 | 72 | 11 Channel 73 | 74 | 12 Stream 75 | 76 | 13 Correlation Id 77 | 78 | 14 IPC 79 | 80 | 15 Congestion Control & Flow Control 81 | 82 | 16 JAVA、C++、C版本异同 83 | 84 | 17 Archive 85 | 86 | 87 | # 1 引言 88 | 89 | Aeron提供可靠、高吞吐、延迟可期的UDP传输,项目开源地址:[Aeron](https://github.com/real-logic/aeron) 90 | 91 | 本文期望用浅显易懂的语言,讲解aeron的概念、原理和技巧。 92 | 93 | 我本人怀着欣赏的心情、夹杂着一点点批判的态度,审视aeron的设计哲学,剖析实现技巧,尽量不做源码分析,尽量不装逼,最好能点到即止。 94 | 95 | 至于aeron有多强悍、适用场景有多广泛、owner背景有多深,这种吹牛逼的事情留给aeron owner吧,本文行文目的不在此。 96 | 97 | 适当地,本文会先介绍一点背景知识,比如UDP通讯,共享内存通讯、多线程等,选择性地而非面面俱到介绍,请酌情阅读或跳过。 98 | 99 | # 2 基础知识:UDP通讯 100 | 101 | ## 2.1 通讯五元组 102 | 103 | 凡用套接字(socket)通讯的,必涉及到通讯五元组:(源IP,源端口,目标IP,目标端口,协议族),也称为通讯三元组:(源EndPoint,目标EndPoint,协议族),此处EndPoint就是一对(IP,端口)。 104 | 105 | 通俗来讲,就是要描述通讯的关键要素:发送者、接收者、双方约定的通讯协议。 106 | 107 | 对于UDP通讯而言,协议族已确定。 108 | 109 | 对于目标端而言,IP和端口通常需要事先指定(除非是在类似p2p穿透场景下的猜端口,或者由第三方协助交换端口信息,本文不考虑此类场景),这类似于服务器的概念,即目标端是在特定的IP和端口处监听的UDP Server,因此需要先绑定(bind)一下。 110 | 111 | 对于源端而言,发送后不管,因此用什么端口无所谓,内核和协议栈关心罢了。通常会事先创建一个套接字,拥有一个随机的端口,后面的发送通过该套接字发送即可。 112 | 113 | UDP通讯面向无连接,无须握手,没有流控(flow control),没有拥塞控制(congestion control),没有确认反馈(ack),没有回包,因此是不可靠通讯。要实现可靠UDP通讯,只能在应用层解决这些问题。 114 | 115 | UDP显然是单向通讯,aeron需要反馈机制来实现可靠UDP通讯,因此在通讯双方之间采用一对UDP通讯:一个用来传输数据,一个用来传输控制指令和反馈等。 116 | 117 | 注意,在这个层面上描述通讯关键要素时,原始的通讯五元组中的源和目标的信息熵不够用了,需要升级一下概念:发送者(sender)、接收者(receiver)。 118 | 119 | aeron的发送者,既是源端,向目标端发送数据;又是目标端,在此接收反馈。 120 | 121 | aeron的接收者,既是目标端,在此接收数据;又是源端,向目标端发送反馈。 122 | 123 | 其实就是两个UDP通讯,各行其是。传输数据的,称为数据通道(data channel);传输反馈的,称为控制通道(control channel)。 124 | 125 | 题外话,这种数据与控制分离的玩法的祖师爷当属高通,在无线通讯领域,高通最早弄出来信令面(signal plane)与用户面( user plane)分离的概念,影响深远。 126 | 127 | ## 2.2 UDP单播 128 | 129 | UDP单播(unicast)是指点到点通讯,发送端向特定的接收端发送。非组播和广播的(IP)地址,都可以用作单播(IP)地址。 130 | 131 | UDP单播套用一下通讯五元组,不难理解。 132 | 133 | 134 | ## 2.3 UDP组播 135 | 136 | UDP组播(multicast)是指一点发、多点收通讯,发送端把数据放到组播地址上,各接收端自己收。 137 | 138 | UDP组播地址是特定的D类地址,即224.0.0.0至239.255.255.255之间,进一步划分如下: 139 | 140 | (1)局域网组播地址:在224.0.0.0~224.0.0.255之间。路由器不对外转发。 141 | 142 | (2)广域网组播地址:在224.0.1.0~238.255.255.255之间。 143 | 144 | (3)管理权限组播地址:在239.0.0.0~239.255.255.255之间,局域网内使用,路由器不对外转发。 145 | 146 | 再强调一下通讯五元组,发送端往特定的端口发,接收端也必须在同样的端口上收。 147 | 148 | ## 2.4 UDP广播 149 | 150 | UDP广播(broadcast)是指一点发,不知道谁收通讯,发送端把数据放到广播地址上,谁爱收不收。 151 | 152 | UDP广播地址为255.255.255.255,仅限于局域网内,交换机不转发。 153 | 154 | UDP广播是UDP组播的一个特例,这得从IP包携带的目标端MAC地址来看:目标端MAC地址的第48比特如果是1,则为组播地址;目标端MAC地址的比特全是1,则为广播地址;而单播,则就是目标端真实的MAC地址。 155 | 156 | 题外话:在局域网内,在交换机的同一个网段,只要想收,无论单播、组播、广播数据,你总能用杂凑模式(promiscuous mode)收到(想想抓包工具),不说那点龌龊之事了。 157 | 158 | ## 2.5 Aeron中的单播 159 | 160 | 与UDP单播无异,不过是一对UDP通讯罢了。 161 | 162 | ## 2.6 Aeron中的组播 163 | 164 | 有两种,一种与UDP组播无异,不过是发送端有一个监听端口,接收端需要往这个端口发送反馈罢了;另一种是模拟组播,aeron中称之为MDC(Multi-Destination-Cast),发送端和接收端之间都是UDP单播,只是发送端发送数据时,要向所有的接收端发送,另外,发送端有一个监听端口,接收端需要往这个端口发送反馈。 165 | 166 | ## 2.7 Aeron中的广播 167 | 168 | aeron没有用UDP广播,和它没啥关系。aeron中的广播是面向客户端(client)的共享内存通讯而言的,是一个单生产者多消费者(spmc)模型。 169 | 170 | ## 2.8 Aeron中的组播地址约定 171 | 172 | aeron为了减少一点配置量(太多了),约定:用来传数据的组播地址(data endpoint)必须是奇数,用来传控制的组播地址(control endpoint)为该地址加1。 173 | 174 | 例如,如果配置224.10.9.7为data endpoint,那么aeron自动将224.10.9.8作为control endpoint。 175 | 176 | 177 | # 3 基础知识:共享内存通讯 178 | 179 | ## 3.1 命名共享内存 180 | 181 | 通常共享内存通讯采用的是命名共享内存,看起来像是POSIX文件系统中的一个文件。进程或者线程通过mmap的方式,将该文件映射到所在的进程空间,进行读写操作,完成数据交换。 182 | 183 | ## 3.2 共享内存目录 184 | 185 | Linux的发行版带有一个/dev/shm的共享内存目录,只是不在硬盘上,而是在内存中,支持shell命令,如df, rm等。在这个目录下创建的子目录、文件,依然是在共享内存中,这为组织和管理共享内存提供了便利。 186 | 187 | aeron正是利用了共享内存目录,达到高效通讯、不受实现语言的限制、不受通讯参与方数量限制的目的,按aeron协议约定交换数据即可。 188 | 189 | aeron的共享内存,主要用于与外界,即客户端(client),的交互。 190 | 191 | ## 3.3 原子操作 192 | 193 | 共享内存属于临界资源,不可避免的需要原子操作,aeron采用内存屏障技术达到性能影响最小化目标。关于这部分代码,建议好好阅读、理解和剽窃。 194 | 195 | aeron在共享内存上,建立单生产者多消费者(spmc)模型、多生产者单消费者(mpsc)模型,采用Log Buffer和Ring Buffer数据结构。 196 | 197 | # 4 基础知识:多线程通讯 198 | 199 | ## 4.1 多线程通讯 200 | 201 | 在一个进程中开多个线程,共同拥有一个进程空间。 202 | 203 | 多线程通讯的优点是可以不通过message交换数据,直接内存访问即可,缺点是需要通过锁、原子操作解决临界资源竞争问题。 204 | 205 | 虽然message与锁等价,但message交换需要编解码(encoding & decoding),对于特定的业务场景,如果锁的开销和复杂度远远小于message交换,那么多线程模型就是适用的。 206 | 207 | 具体问题具体分析,不一而足。 208 | 209 | ## 4.2 aeron中的多线程 210 | 211 | aeron选择多线程模型。aeron内部主要有三个的角色:conductor、sender、receiver。aeron抽象出runner的概念,每个角色交给一个runner执行,每个runner的执行承载体为线程。 212 | 213 | 由此,aeron提供了若干种模式: 214 | 215 | * shared模式:三个runner在同一个线程上执行 216 | 217 | * shared network模式:condctor runner一个线程,sender runner + receiver runner一个线程 218 | 219 | * dedicated模式:三个runner分别在三个不同的线程上执行 220 | 221 | 交互发生在conductor和sender之间、conductor和receiver之间,通过单生产者单消费者(spsc)队列交换数据。 222 | 223 | # 5 Design Overview 224 | 225 | 先从剖析aeron的架构图开始。 ![Architecture](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/aeron/architecture.png) 226 | 227 | 228 | 这张图画得挺炫,但有蛊惑之处,需要探究一番。 229 | 230 | ## 5.1 Media 231 | 232 | 图的最中间为Media,传输媒介。通俗地说,凡是支持套接字UDP接口的,比如sendmsg,都可以作为aeron的Media。老外忽悠起来一套一套的,逼格很高。 233 | 234 | ## 5.2 Media Driver 235 | 236 | 紧挨着Media的是Media Driver,这是一个抽象的概念,是aeron的核心,由三个角色组成:conductor、sender、receiver。 237 | 238 | (1)conductor与client(客户端)通过共享内存交互(Ring Buffer),从client接收指令、将反馈广播给客户端。 239 | 240 | 指令集:暂略 241 | 242 | (2)conductor与sender通过spsc队列,向sender发送指令 243 | 244 | 指令集:暂略 245 | 246 | (3)conductor与receiver通过spsc队列,向receiver发送指令 247 | 248 | 指令集:暂略 249 | 250 | (4)sender负责向Media发送UDP报文 251 | 252 | 那么问题来了:sender发送的数据来自哪里?答案是:共享内存publication。 253 | 254 | 也就是说,sender一直在检查publication,如果有数据,就以UDP报文的形式发送出去。 255 | 256 | (5)receiver负责从Media接收UDP报文 257 | 258 | 同样的问题:receiver接收到的数据存哪里去了?答案是:共享内存image。 259 | 260 | 即,receiver每收到一个UDP报文,就会写入image,写满后rotate一下,继续写,甭管有没有人要,反正放这里了。 261 | 262 | 这里留下两个问题:谁在往publication里写入数据?谁在从image中读取数据? 263 | 264 | ## 5.3 Client 265 | 266 | 图的左右两侧为客户端(client),可以理解为它们并不是aeron组件,虽然aeron提供了JAVA版和C++版客户端,可以封装进Application中。 267 | 268 | 客户端主要的功能有三个: 269 | 270 | (1)通过Log Buffer共享内存,向Media Driver写入数据(publish) 271 | 272 | (2)通过Log Buffer共享内存,从Media Driver读出数据(subscribe) 273 | 274 | (3)通过Ring Buffer共享内存,向Media Driver发送指令(command),以及接受反馈(response) 275 | 276 | 注意,publish操纵的Log Buffer共享内存称之为publication,subscribe操作的Log Buffer共享内存称之为image,指令与反馈操作的Ring Buffer共享内存是CNC(command and control)。 277 | 这是三个不同的共享内存,彼此功能独立,严格区分,各司其职。 278 | 279 | 这张图蛊惑之处主要在客户端(client),以下分析纯属个人意见,并不强求大家认同: 280 | 281 | (1)客户端(client)的publisher、subscriber、conductor并不精确。 282 | 283 | client的conductor功效并不等同于Media Driver中的conductor,不是一回事。client conducotr主要是把控制指令写入共享内存CNC中,并从CNC中读取反馈。 284 | 因此,client只要按aeron要求提供写入、读取的功能即可与Media Driver的conductor正常交互,其实现既可利用aeron提供的不同语言版本的接口,也可自己实现。 285 | 286 | client中的publisher和subscriber存在类似的问题,分别是与共享内存publication和image进行读写交互。从pub-sub模型来看,通常,一方pub,多方sub,说的是应用层参与方的事。 287 | 而client在此处仅能表达与共享内存的交互,不够精确。 288 | 289 | 个人认为应该将client中的publisher和subscriber换成另外两个角色:writer和reader。publisher和subscriber则留在应用层。 290 | 应用层建立好pub-sub关系后,具体数据流为: 291 | 292 | 应用层publisher将数据交给client writer => client writer将数据写入共享内存publication => Media Driver的sender从publication取出数据,放到Media上,发往远端的subscriber。这是pub数据流。 293 | 294 | 远端Media Driver的receiver从Media接收数据,放进共享内存image => client reader从共享内存image中读取数据,交给应用层。 这是sub数据流。 295 | 296 | 如果把应用层和客户端看作是一回事,即都是client,那么这张图没有毛病,只是理解上会断片。 297 | 298 | 299 | (2)客户端(client)同时存在publisher、subscriber不准确。 300 | 301 | 客户端同时画上publisher和subscriber能理解,协议嘛,收发两端都要有。在图的左右两侧都画上客户端,每个客户端都画上publisher和subscriber,这就不理解了。这是要告诉读者,客户端必须同时支持publisher和subscriber?实际应用场景显然不是,通常是一端需要publisher,另一端需要subscriber就够了。把图右侧的Media Driver和Client去掉,理解上一点毛病都没有,协议完整。 302 | 303 | 无论如何,读者理解了原理,这张图怎么看都行,它只是对初学者会造成一些不必要的理解上的麻烦。 304 | 305 | 从Media Driver角度看,sender和receiver与UDP通讯的关系:sender是UDP的发送端,receiver是接收端,receiver负责监听指定的udp端口。 306 | 307 | 从应用角度看,publisher和subscriber与UDP通讯的关系:publisher为应用层数据发送端,对应Media Driver的sender,subscriber为应用层数据接收端,对应Media Driver的receiver,因此可以简单理解为,publisher往特定的UDP通道上发送数据,subscriber在特定的UDP通道上接收数据。 308 | 309 | 理解就好了,注意所站的角度。 310 | 311 | 根据前面的一系列探究,我们可以延伸断定: 312 | 313 | (1)Media Driver不支持relay(中继)能力,因为发送的数据来自publication,接收的数据写入image,二者严格分开。 314 | 315 | (2)Media Driver不可能是一个自治的系统,因为传输数据来源于client,去向也是client。 316 | 317 | 好,再来看如果只有一个Media Driver,publisher和subscriber都与它交互,那么sender是如何把数据交给receiver的? 318 | 319 | 答案是对于UDP Media而言走UDP通道:sender把数据放到UDP通道,receiver从UDP通道上接收数据,不论单播还是组播,因为sender和receiver在Media Driver内没有交互。 320 | 321 | 322 | # 6 Ring Buffer 323 | 324 | Ring Buffer(环形缓冲,也称为Circular Buffer)结构,是一个FIFO的环形队列。 325 | 326 | Media Driver的conductor和client之间通讯通过共享内存CNC的Ring Buffer完成。CNC的结构如下图所示: 327 | 328 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/aeron/CNC.png) 329 | 330 | 可见通讯是双向的:CNC有两个Ring Buffer,一个是client到conductor方向,多生产者单消费者(mpsc)模型;一个是conductor到client方向,广播方式,单生产者多消费者(spmc)模型。 331 | 332 | 特别指出,aeron的作者在实现Ring Buffer时很务实,充满实战既视感,毫不学究。 333 | 334 | 从网上盗来一图如下。Ring Buffer有一个写指针和一个读指针,顺序读写。关键不同点在遇到这段内存的尾部和头部时的处理。 335 | 336 | 337 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/aeron/Circular%20Buffer.jpg) 338 | 339 | 对标准的Ring Buffer而言,遇到头尾,数据逻辑上顺序连接,实际上一段数据在这段内存的尾部,一段数据在这段内存的头部,不连续,读写数据需要两次操作。 340 | aeron的实现是,遇到尾部空间不足,则直接跳过,从头部开始,读写一次操作完成。 341 | 342 | 这儿有一版很好的Ring Buffer开源实现,供读者参考:[Ring Buffer](https://github.com/VladimirTyrin/RingBuffer.git) 343 | 344 | 345 | # 7 Log Buffer 346 | 347 | aeron的Log Buffer结构,用于conductor与client之间交换数据时的共享内存publication和image中。其结构如下图所示: 348 | 349 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/aeron/publication%20log%20buffer%E7%BB%93%E6%9E%84.png) 350 | 351 | 352 | Log Buffer主要由三部分组成:3个partition,1个Meta Data、1个Default Frame Header。 353 | 354 | Log Buffer Partition存的是数据部,由若干定长的Term组成。 355 | 356 | Log Buffer Meta Data用来指示每个Partition存的是哪个term的哪部分数据。 357 | 358 | 对于publication,Log Buffer是多生产者、单消费者模型(mpsc);对于image而言,Log Buffer是单生产者、多生产者模型(spmc)。 359 | 360 | aeron处理的是数据流,因此Log Buffer数据的读写是顺序的。数据流被切成固定大小的Term,放进Log Buffer的3个partion中。 361 | 每个Term有一个标识(term id),标识从初始值开始,顺序增长,aeron根据该标识,对3个partition采用Round Robbin算法, 362 | 比如Term A放进partion[0],那么Term A+1放进partion[1], Term A+2放进partion[2],Term A+3放进partion[0],依次类推。 363 | 这样做的好处是可以降低读写竞争概率,比如receiver将Term A放进partion[0]后,reader才可以从这里读,而此时receiver可以将Term A+1放进partion[1],对不同partition的内存屏障大概率不会产生竞争。 364 | 365 | 366 | # 8 Frame 367 | 368 | Frame(帧)是sender和receiver之间交互的唯一方式,无论它们是否属于同一个Media Driver。 369 | 370 | aeron定义了六种Frame: Setup Frame、Data Frame、NAK Frame,Status Frame,RTT Frame, Error Frame。 371 | 372 | 其中,Data Frame根据标志和帧长的不同,又包含PAD Frame和Heartbeat Frame。 373 | 374 | 一个Frame就是一条aeron定义的Message(消息),包含头部和数据部,有明确的边界。其定义可参考[Protocol Specification](https://github.com/real-logic/aeron/wiki/Protocol-Specification) 375 | 376 | 377 | ## 8.1 Setup Frame 378 | 379 | Setup Frame的目的是为了在sender和receiver之间建立起一个双向通讯(一对UDP通讯),又称之为一个session(会话)。 380 | 381 | 如果sender和receiver之间采用UDP单播通讯,那么建立的基本过程是,sender发送Setup Frame,receiver回Status Frame,如下图所示: 382 | 383 | 384 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/aeron/%E5%8D%95%E6%92%AD%E4%BA%A4%E4%BA%92%E6%B5%81%E7%A8%8B.png) 385 | 386 | 387 | 如果sender和receiver之间采用UDP组播通讯,那么建立的基本过程是:receiver从数据组播通道上“听”到Data Frame后,向控制组播通道上发送携带setup标志的Status Frame, 388 | sender收到该帧后,在控制组播通道上发送Setup Frame,然后receiver在单播通道上回Status Frame。如下图所示: 389 | 390 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/aeron/%E7%BB%84%E6%92%AD%E4%BA%A4%E4%BA%92%E6%B5%81%E7%A8%8B.png) 391 | 392 | 393 | ## 8.2 Status Frame 394 | 395 | Status Frame由receiver发送给sender,告诉对方,我方的数据收到了多少(收到什么位置了),我方的接收窗口多大等信息,sender据此来做拥塞控制和流控。 396 | 397 | 398 | ## 8.3 Data Frame 399 | 400 | Data Frame顾名思义,是用来传输应用层数据的,携带的信息包括,本次传输的数据是从什么位置开始的,长度是多少等。 401 | 402 | 通常Data Frame在数据通道上传输,但是Heartbeat Frame在控制通道上传输。 403 | 404 | 405 | ## 8.4 PAD Frame 406 | 407 | PAD Frame是一个特殊的Data Frame:有长度,没数据(无payload)。 408 | 409 | 这是因为应用层要传输的数据的长度未必正好是Term的整数倍,最后一段数据在一个Data Frame中传递完后,不足一个Term的部分,用PAD Frame传递,指示receiver数据传完了。 410 | 411 | 412 | ## 8.5 Heartbeat Frame 413 | 414 | Heartbeat Frame(心跳帧)也是一个特殊的Data Frame:没长度(为0),没数据(无payload)。 415 | 416 | aeron会检查流(stream)的活性,如果一段时间没有数据传输,aeron就会停止流,删除对应的Log Buffer等。 417 | 418 | 因此,在应用层无数据传输,又要保持流(stream)不被删,就需要发送Heartbeat Frame。 419 | 420 | 技巧性地,在aeron的实现中,如果要关闭流,直接停止心跳,或者让aeron以为心跳停止即可。 421 | 422 | 423 | ## 8.6 NAK Frame 424 | 425 | NAK Frame是实现UDP可靠传输的重要手段之一。receiver通过NAK Frame明确告诉sender,哪个数据帧需要重传,即需要重传的数据从什么位置开始,长度是多少等。 426 | 427 | 428 | ## 8.7 RTT Frame 429 | 430 | RTT Frame用来测量sender和receiver之间的往返延迟。该帧在JAVA版本中支持,在C版本中尚未支持。 431 | 432 | 433 | ## 8.8 Error Frame 434 | 435 | Error Frame用来指示错误信息的帧。 436 | 437 | 438 | # 9 Term 439 | 440 | aeron将应用层收发数据看做数据流,并将其按固定长度切成连续的Term,并唯一顺序编号。 441 | 442 | aeron为了简化后续的各种换算与定位,约定此固定长度必须为2的N次方。 443 | 444 | 应用层数据中的一个字节在数据流中的位置,姑且称之为packet position。另一方面,该字节在编号为term id、偏移量为term offset的Term中。 445 | 446 | 假设Term的起始编号为initial term id;假设Term的固定长度为term buffer length,为2的position bits to shift次方。 447 | 448 | 那么,Term中的位置到数据流中的位置的换算关系如下: 449 | 450 | 451 | packet position = (term id - initial term id) * (term buffer length) + (term offset) 452 | = ((term id - initial term id) <<(position bits to shift) ) + (term offset) 453 | = (term id - initial term id) | (term offset) 454 | 455 | 注:低位部占(position bits to shift)个比特 456 | 457 | 反过来,数据流中的位置到Term中的位置的换算关系如下: 458 | 459 | term offset = (packet position)的低位部(position bits to shift)个比特 = (packet position) & mask, 这里 mask = (1 << (position bits to shift)) - 1 460 | term id = (packet position)的高位部 + (initial term id) = ((packet position) >> (position bits to shift)) + (initial term id) 461 | 462 | 可见,Term长度固定为2的N次方后,换算关系可采用位操作进行。 463 | 464 | 根据term id到Log Buffer的partition之间的映射关系,字节也可以对应到partition,不再累述。 465 | 466 | # 10 Fragment 467 | 468 | 应用程序数据(APDU,Application Data Unit),被切成多个Fragment,每个Fragment作为payload放到一个Frame里传输。 469 | 470 | Term与Fragment的关系: 471 | 472 | 一个Term由多个Fragment组成,即发送端将Term拆成多个Fragment,接收端将多个Fragment拼装成Term。 473 | 474 | 一个Frame只能承载一个Term中的一段数据(Fragment)。 475 | 476 | Term是固定长度,Fragment没有长度要求,可以根据拥塞控制、流控、窗口大小动态调整。所以,Fragment更多的只是个概念,形而上,来描述Frame的payload罢了。 477 | 478 | # 11 Channel 479 | 480 | Channel用来描述一个Media上的一个数据流(stream),aeron中用URI来标识Channel。通俗地讲,Channel核心是二元组(media,endpoint)。 481 | 482 | 典型URI举例: 483 | 484 | aeron:udp?endpoint=192.168.0.1:40456 485 | 486 | 这里, aeron:udp是一种media,endpoint由ip地址和端口号构成。 487 | 488 | 更详尽的URI定义与例子,请参阅[Channel Configuration](https://github.com/real-logic/aeron/wiki/Channel-Configuration) 489 | 490 | 491 | # 12 Stream 492 | 493 | 一个Channel上可以承载多个stream(数据流),用不同的stream id来标识。 494 | 495 | 496 | # 13 Correlation Id 497 | 498 | aeron有不少标识,这里特别提到Correlation Id,是因为该标识与Client(这里不区分Client和Application)高度相关。 499 | 500 | 通常,Client生成Correlation Id,在指令中携带,传递给Media Driver,后者在反馈中携带,回送给Client。换言之,这原本是Client用来区分和匹配(指令,反馈)对的。 501 | 502 | 但是, aeron目前的实现中,将Correlation Id挪作它用了。比如,在create publication时,Media Driver将Correction Id直接作为publication的Registration Id,并将该Registration Id回送给Client留作纪念,用来标识所创建的publication。 503 | 504 | 这下麻烦了!Media Driver可能会面对多个Client,如果两个Client用同一个Correlation Id创建publication,这不冲突了吗? 505 | 506 | 为了缓解这个问题,aeron把球抛给了client:各位client,请务必保证Correlation Id全局唯一! 507 | 508 | aeron设计上的一个缺陷,导致在这儿挖了个坑,各位小心点,绕道走就是。 509 | 510 | 511 | # 14 IPC 512 | 513 | 在Channel二元组(media, endpoint)中,如果media是aeron:udp,那么就是UDP Media;如果media是aeron:ipc,那么就是IPC Media。 514 | 515 | 如果共享内存用来在同一台设备上的进程或线程间通讯,那么就可以用IPC Media来提高性能,获得更高的吞吐量、更低的延迟。 516 | 517 | 518 | # 15 Congestion Control & Flow Control 519 | 520 | aeron的C版这块实现基本为零,非常简陋:不能调整窗口大小。JAVA版这块我还没研究。 521 | 522 | 我认为,可靠UDP传输最有价值的,大概就是在这里了,目测aeron owner不会很快放出来。各位想在aeron上玩点大的,就在这儿下功夫吧! 523 | 524 | 525 | # 16 JAVA、C++、C版本异同 526 | 527 | 无论是什么语言的版本,aeron都是在搞同一件事: 操作共享内存。 528 | 529 | aeron最早出的是JAVA版,也是目前为止功能最全的版本。C版是将aeron的理念重新实现一遍,aeron协议完全一致。 530 | 531 | 剩下的,就是各路神仙封装出来的client:JAVA版本client,C++版本client,.NET版本client。 532 | 533 | 这些client都是用来操作Media Driver面向client端的共享内存的:CNC,publication,image。 534 | 535 | 由于Media Driver中runner的概念,导致Media Driver事实上既可以独立部署,也可以嵌套在Client或Application中, 536 | 于是有好事之徒,在Media Driver之上又包装一层,加上自己的指令集、流程控制,形成新的Media Driver。下面的Archive就是一例。 537 | 538 | 539 | # 17 Archive 540 | 541 | Archive的应用场景是录播(Recording)、重放(Replay),也就是,Media Driver将收到的数据,不仅放入image,还要额外落盘到文件,重放时从落盘文件中取出数据,放到指定的Channel上。 542 | 543 | Aeron社区目前提供了Archive Media Driver和Archive Client。 544 | 545 | Archive既可以单机单Media Driver部署,也可以单机双Media Driver部署,也可以双机双Media Driver部署。本质上,你只要能满足archive定义的指令集和处理流程即可。 546 | 双Media Driver部署时,一个用Archive Media Driver,一个用普通的Media Driver就够了,不必非要用两个Archive Media Driver。 547 | 548 | 下图是不同部署方式的原理图,供参考,不再累述原理。 549 | 550 | 单机单Media Driver: 551 | 552 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/aeron/archive%E5%8E%9F%E7%90%86-%E5%90%8C%E6%9C%BA%E9%83%A8%E7%BD%B2.png) 553 | 554 | 555 | 单机双Media Driver,或双机双Media Driver: 556 | 557 | ![](https://github.com/chaoyongzhou/Knowledge-Sharing/blob/master/aeron/archive%E5%8F%8C%E6%9C%BA%E9%83%A8%E7%BD%B2.png) 558 | 559 | 560 | -------------------------------------------------------------------------------- /xcache粘合ngx upstream.md: -------------------------------------------------------------------------------- 1 | # xcache粘合ngx upstream 2 | 3 | ## 1、背景 4 | 5 | 按照xcache原本的设计,需要部署一个独立的detect组件做源站探测,xcache在回源前截住dns解析,向detect组件发送tcp解析请求,携带待解析的域名;detect组件根据配置的IP、域名、策略周期解析源站的存活,并根据服务过来的请求,返回一个IP地址。 6 | 7 | 考虑到对ngx upstream配置的直接支持,xcache决定采取粘合方式。 8 | 9 | 困难点在于proxy在ngx content阶段介入,向源站发起请求,而xcache需要在ngx content阶段介入,自己向源站发起请求,需要掐掉upstream建连部分。 10 | 11 | 因此,xcache需要解决的问题是,粘合upstream,只利用其配置、算法选择回源IP地址,而不走upstream的回源流程。 12 | 13 | ## 2、upstream原理分析 14 | 15 | ### 2.1、upstream模块 16 | 17 | 以tengine 2.3.2为例,与ngx http相关的upstream模块如下: 18 | 19 | 原生的顶层upstream模块: 20 | 21 | src/http/ngx_http_upstream.c 22 | src/http/ngx_http_upstream_round_robin.c 23 | 24 | 原生的upstream算法模块: 25 | 26 | src/http/modules/ngx_http_upstream_hash_module.c 27 | src/http/modules/ngx_http_upstream_ip_hash_module.c 28 | src/http/modules/ngx_http_upstream_keepalive_module.c 29 | src/http/modules/ngx_http_upstream_least_conn_module.c 30 | src/http/modules/ngx_http_upstream_random_module.c 31 | src/http/modules/ngx_http_upstream_zone_module.c 32 | 33 | 扩展upstream辅助功能模块: 34 | 35 | modules/ngx_http_upstream_check_module/ngx_http_upstream_check_module.c 36 | modules/ngx_http_upstream_consistent_hash_module/ngx_http_upstream_consistent_hash_module.c 37 | modules/ngx_http_upstream_dynamic_module/ngx_http_upstream_dynamic_module.c 38 | modules/ngx_http_upstream_keepalive_module/ngx_http_upstream_keepalive_module.c 39 | modules/ngx_http_upstream_session_sticky_module/ngx_http_upstream_session_sticky_module.c 40 | modules/ngx_http_upstream_vnswrr_module/ngx_http_upstream_vnswrr_module.c 41 | modules/ngx_multi_upstream_module/ngx_http_multi_upstream.c 42 | modules/ngx_multi_upstream_module/ngx_http_multi_upstream_module.c 43 | modules/ngx_multi_upstream_module/ngx_multi_upstream_module.c 44 | 45 | ### 2.2、源码分析 46 | 47 | 从配置和源码角度,来分析upstream原理。nginx是通过配置的不同,在运行期间动态结合handler来获得不同能力的。 48 | 49 | 就从指令proxy_pass入手。 50 | 51 | #### 2.2.1、指令proxy_pass配置 52 | 53 | upstream配置举例: 54 | 55 | upstream www.test.com { 56 | server 192.168.1.146:80 max_fails=3; 57 | server 192.168.1.147:80 max_fails=3; 58 | 59 | keepalive 1000; 60 | keepalive_timeout 300s; 61 | keepalive_requests 10000; 62 | } 63 | 64 | server的location配置举例: 65 | 66 | location / { 67 | set $upstream_name "www.test.com"; 68 | proxy_pass http://$upstream_name; 69 | } 70 | 71 | #### 2.2.2、指令proxy_pass入口源码 72 | 73 | 指令proxy_pass配置解析入口为ngx\_http\_proxy\_pass,指明了运行态content phase的handler为ngx\_http\_proxy\_handler。 74 | 75 | static char * 76 | ngx_http_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 77 | { 78 | ngx_http_proxy_loc_conf_t *plcf = conf; 79 | ngx_http_core_loc_conf_t *clcf; 80 | 81 | ...... 82 | clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); 83 | clcf->handler = ngx_http_proxy_handler; 84 | ...... 85 | } 86 | 87 | 在运行态,ngx\_http\_proxy\_handler做了三件事情: 88 | 89 | > (1) 创建一个空的upstream (ngx\_http\_upstream\_t *u),并挂到当前请求下(r->upstream = u),由ngx\_http\_upstream\_create完成 90 | > 91 | > (2) 设定upstream的create\_request handler (u->create_request = ngx\_http\_proxy\_create\_request) 92 | > 93 | > (3) 调用upstream的初始化(ngx\_http\_read\_client\_request\_body => ngx\_http\_upstream\_init => ngx\_http\_upstream\_init\_request) 94 | 95 | 96 | 97 | static ngx_int_t 98 | ngx_http_proxy_handler(ngx_http_request_t *r) 99 | { 100 | ngx_http_upstream_t *u; 101 | ngx_http_proxy_loc_conf_t *plcf; 102 | 103 | if (ngx_http_upstream_create(r) != NGX_OK) { 104 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 105 | } 106 | plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module); 107 | u = r->upstream; 108 | ..... 109 | 110 | u->create_request = ngx_http_proxy_create_request; 111 | .... 112 | rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init); 113 | if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { 114 | return rc; 115 | } 116 | return NGX_DONE; 117 | } 118 | 119 | ngx_int_t 120 | ngx_http_upstream_create(ngx_http_request_t *r) 121 | { 122 | ngx_http_upstream_t *u; 123 | u = r->upstream; 124 | if (u && u->cleanup) { 125 | r->main->count++; 126 | ngx_http_upstream_cleanup(r); 127 | } 128 | u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t)); 129 | if (u == NULL) { 130 | return NGX_ERROR; 131 | } 132 | r->upstream = u; 133 | ...... 134 | } 135 | 136 | upstream的初始化也做了三件事情: 137 | 138 | (1)根据proxy_pass指令中解析得到的host名称,从全局的upstream配置解析(umcf)中查找到对应的upstream配置(uscf),并挂入前面新创建的upstream中(u->upstream = uscf) 139 | 140 | (2)调用查找到的uscf的peer初始化handler(uscf->peer.init(r, uscf)),这个handler的设定与扩展辅助模块有关,下面再表。 141 | 142 | (3)调用connect(ngx\_http\_upstream\_connect => ngx\_event\_connect\_peer)开始发起到对端peer的连接(即源站或上一层),开始后续流程。 143 | 144 | static void 145 | ngx_http_upstream_init_request(ngx_http_request_t *r) 146 | { 147 | ngx_str_t *host; 148 | ngx_http_upstream_t *u; 149 | ngx_http_core_loc_conf_t *clcf; 150 | ngx_http_upstream_srv_conf_t *uscf, **uscfp; 151 | ngx_http_upstream_main_conf_t *umcf; 152 | if (r->aio) { 153 | return; 154 | } 155 | u = r->upstream; 156 | if (u->resolved == NULL) { 157 | uscf = u->conf->upstream; 158 | } else { 159 | host = &u->resolved->host; 160 | umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module); 161 | uscfp = umcf->upstreams.elts; 162 | for (i = 0; i < umcf->upstreams.nelts; i++) { 163 | uscf = uscfp[i]; 164 | if (uscf->host.len == host->len 165 | && ((uscf->port == 0 && u->resolved->no_port) 166 | || uscf->port == u->resolved->port) 167 | && ngx_strncasecmp(uscf->host.data, host->data, host->len) == 0) 168 | { 169 | goto found; 170 | } 171 | } 172 | ...... 173 | found: 174 | if (uscf == NULL) { 175 | ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, 176 | "no upstream configuration"); 177 | ngx_http_upstream_finalize_request(r, u, 178 | NGX_HTTP_INTERNAL_SERVER_ERROR); 179 | return; 180 | } 181 | u->upstream = uscf; 182 | if (uscf->peer.init(r, uscf) != NGX_OK) { 183 | ngx_http_upstream_finalize_request(r, u, 184 | NGX_HTTP_INTERNAL_SERVER_ERROR); 185 | return; 186 | } 187 | ...... 188 | ngx_http_upstream_connect(r, u); 189 | } 190 | 191 | ngx\_http\_upstream\_connect发起socket连接后,向对端发起请求,这些正是xcache粘合upstream需要掐掉的部分。 192 | 193 | 检视代码,可以看到一个重要的信息:对端信息就藏再u->peer.sockaddr中。所以,我们需要拿到peer即可。考虑到u是新创建的空的upstream (ngx\_http\_upstream\_t),所以, peer只可能、也应该来自于前面查找到的uscf。所以调用点uscf->peer.init(r, uscf)是关键。 194 | 195 | void 196 | ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u) 197 | { 198 | ....... 199 | rc = ngx_event_connect_peer(&u->peer); 200 | ...... 201 | ngx_http_upstream_send_request(r, u, 1); 202 | } 203 | 204 | 205 | uscf->peer.init这个handler的设定,取决于upstream配置块。 206 | 207 | 从2.2.1的配置来看,这是走的原生keepalive upstream模块。指令keepalive的解析入口为ngx\_http\_upstream\_keepalive,其设定uscf->peer.init_upstream的handler为ngx\_http\_upstream\_init\_keepalive。 208 | 209 | static char * 210 | ngx_http_upstream_keepalive(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 211 | { 212 | ngx_http_upstream_srv_conf_t *uscf; 213 | ngx_http_upstream_keepalive_srv_conf_t *kcf = conf; 214 | 215 | /* init upstream handler */ 216 | uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); 217 | kcf->original_init_upstream = uscf->peer.init_upstream 218 | ? uscf->peer.init_upstream 219 | : ngx_http_upstream_init_round_robin; 220 | uscf->peer.init_upstream = ngx_http_upstream_init_keepalive; 221 | return NGX_CONF_OK; 222 | } 223 | 224 | 225 | 226 | 而uscf->peer.init_upstream是在创建nginx的主配置时调用的,即将upstream配置加入到nginx配置中。 227 | 228 | static ngx_http_module_t ngx_http_upstream_module_ctx = { 229 | ngx_http_upstream_add_variables, /* preconfiguration */ 230 | NULL, /* postconfiguration */ 231 | ngx_http_upstream_create_main_conf, /* create main configuration */ 232 | ngx_http_upstream_init_main_conf, /* init main configuration */ 233 | NULL, /* create server configuration */ 234 | NULL, /* merge server configuration */ 235 | NULL, /* create location configuration */ 236 | NULL /* merge location configuration */ 237 | }; 238 | static char * 239 | ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf) 240 | { 241 | ngx_http_upstream_main_conf_t *umcf = conf; 242 | ngx_http_upstream_srv_conf_t **uscfp; 243 | 244 | uscfp = umcf->upstreams.elts; 245 | for (i = 0; i < umcf->upstreams.nelts; i++) { 246 | init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream: 247 | ngx_http_upstream_init_round_robin; 248 | if (init(cf, uscfp[i]) != NGX_OK) { 249 | return NGX_CONF_ERROR; 250 | } 251 | } 252 | ...... 253 | return NGX_CONF_OK; 254 | } 255 | 256 | 257 | 258 | 再回头看具体到一个keepalive upstream配置的初始化入口ngx\_http\_upstream\_init\_keepalive,其设定uscf->peer.init的handler为ngx\_http\_upstream\_init\_keepalive\_peer。 259 | 260 | static ngx_int_t 261 | ngx_http_upstream_init_keepalive(ngx_conf_t *cf, 262 | ngx_http_upstream_srv_conf_t *us) 263 | { 264 | ngx_http_upstream_keepalive_srv_conf_t *kcf; 265 | 266 | kcf = ngx_http_conf_upstream_srv_conf(us, 267 | ngx_http_upstream_keepalive_module); 268 | 269 | 270 | if (kcf->original_init_upstream(cf, us) != NGX_OK) { 271 | return NGX_ERROR; 272 | } 273 | ...... 274 | kcf->original_init_peer = us->peer.init; 275 | us->peer.init = ngx_http_upstream_init_keepalive_peer; 276 | ...... 277 | } 278 | 279 | 开看看ngx\_http\_upstream\_init\_keepalive\_peer的实现,设定了一个重要的handler: r->upstream->peer.get = ngx\_http\_upstream\_get\_keepalive\_peer; 280 | 281 | static ngx_int_t 282 | ngx_http_upstream_init_keepalive_peer(ngx_http_request_t *r, 283 | ngx_http_upstream_srv_conf_t *us) 284 | { 285 | ngx_http_upstream_keepalive_peer_data_t *kp; 286 | ngx_http_upstream_keepalive_srv_conf_t *kcf; 287 | 288 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 289 | "init keepalive peer"); 290 | kcf = ngx_http_conf_upstream_srv_conf(us, 291 | ngx_http_upstream_keepalive_module); 292 | kp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_keepalive_peer_data_t)); 293 | if (kp == NULL) { 294 | return NGX_ERROR; 295 | } 296 | if (kcf->original_init_peer(r, us) != NGX_OK) { 297 | return NGX_ERROR; 298 | } 299 | kp->conf = kcf; 300 | kp->upstream = r->upstream; 301 | kp->data = r->upstream->peer.data; 302 | kp->original_get_peer = r->upstream->peer.get; 303 | kp->original_free_peer = r->upstream->peer.free; 304 | r->upstream->peer.data = kp; 305 | r->upstream->peer.get = ngx_http_upstream_get_keepalive_peer; 306 | r->upstream->peer.free = ngx_http_upstream_free_keepalive_peer; 307 | #if (NGX_HTTP_SSL) 308 | kp->original_set_session = r->upstream->peer.set_session; 309 | kp->original_save_session = r->upstream->peer.save_session; 310 | r->upstream->peer.set_session = ngx_http_upstream_keepalive_set_session; 311 | r->upstream->peer.save_session = ngx_http_upstream_keepalive_save_session; 312 | #endif 313 | return NGX_OK; 314 | } 315 | 316 | 继续跟踪ngx\_http\_upstream\_get\_keepalive\_peer,调用kp->original\_get\_peer获得peer信息,找到connection并挂入peer connection下。 317 | 318 | static ngx_int_t 319 | ngx_http_upstream_get_keepalive_peer(ngx_peer_connection_t *pc, void *data) 320 | { 321 | ngx_http_upstream_keepalive_peer_data_t *kp = data; 322 | ngx_http_upstream_keepalive_cache_t *item; 323 | ngx_int_t rc; 324 | ngx_queue_t *q, *cache; 325 | ngx_connection_t *c; 326 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pc->log, 0, 327 | "get keepalive peer"); 328 | /* ask balancer */ 329 | rc = kp->original_get_peer(pc, kp->data); 330 | if (rc != NGX_OK) { 331 | return rc; 332 | } 333 | /* search cache for suitable connection */ 334 | cache = &kp->conf->cache; 335 | for (q = ngx_queue_head(cache); 336 | q != ngx_queue_sentinel(cache); 337 | q = ngx_queue_next(q)) 338 | { 339 | item = ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue); 340 | c = item->connection; 341 | if (ngx_memn2cmp((u_char *) &item->sockaddr, (u_char *) pc->sockaddr, 342 | item->socklen, pc->socklen) 343 | == 0) 344 | { 345 | ngx_queue_remove(q); 346 | ngx_queue_insert_head(&kp->conf->free, q); 347 | goto found; 348 | } 349 | } 350 | return NGX_OK; 351 | found: 352 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0, 353 | "get keepalive peer: using connection %p", c); 354 | c->idle = 0; 355 | c->sent = 0; 356 | c->log = pc->log; 357 | c->read->log = pc->log; 358 | c->write->log = pc->log; 359 | c->pool->log = pc->log; 360 | if (c->read->timer_set) { 361 | ngx_del_timer(c->read); 362 | } 363 | pc->connection = c; 364 | pc->cached = 1; 365 | return NGX_DONE; 366 | } 367 | 368 | xcache粘合只关心如何获取peer,并不关心connection,因此这里的关注点为original\_get\_peer这个handler是如何被初始化的。 369 | 370 | #### 2.2.3、获取peer 371 | 372 | 梳理代码,original\_get\_peer这个handler在如下接口中被初始化: 373 | 374 | ngx_http_dyups_init_peer 375 | ngx_http_multi_upstream_init_peer 376 | ngx_http_upstream_init_chash_peer 377 | ngx_http_upstream_init_dynamic_peer 378 | ngx_http_upstream_init_dynamic_peer 379 | ngx_http_upstream_init_hash_peer 380 | ngx_http_upstream_init_chash_peer 381 | ngx_http_upstream_init_ip_hash_peer 382 | ngx_http_upstream_init_keepalive_peer 383 | ngx_http_upstream_init_least_conn_peer 384 | ngx_http_upstream_init_random_peer 385 | ngx_http_upstream_init_round_robin_peer 386 | ngx_http_upstream_session_sticky_init_peer 387 | ngx_http_upstream_init_vnswrr_peer 388 | 389 | 逐一分析发现,所有初始化接口都直接或间接(通过handler original\_init\_peer)调用了接口ngx\_http\_upstream\_init\_round\_robin\_peer,这是归一化。 390 | 391 | 那么为什么会归一化到round robin呢?这还需要回头看ngx\_http\_upstream\_init\_keepalive接口中一段未解释的部分,即original\_init\_upstream的调用。 392 | 393 | static ngx_int_t 394 | ngx_http_upstream_init_keepalive(ngx_conf_t *cf, 395 | ngx_http_upstream_srv_conf_t *us) 396 | { 397 | ngx_http_upstream_keepalive_srv_conf_t *kcf; 398 | 399 | kcf = ngx_http_conf_upstream_srv_conf(us, 400 | ngx_http_upstream_keepalive_module); 401 | 402 | 403 | if (kcf->original_init_upstream(cf, us) != NGX_OK) { 404 | return NGX_ERROR; 405 | } 406 | ...... 407 | kcf->original_init_peer = us->peer.init; 408 | us->peer.init = ngx_http_upstream_init_keepalive_peer; 409 | ...... 410 | } 411 | 412 | 即,初始化当前keepalive upstream模块,需要先通过handler original\_init\_upstream初始化它的父模块,这儿体现的是继承性。再回溯指令keepalive的处理接口ngx\_http\_upstream\_keepalive,original\_init\_upstream要么被设定为uscf->peer.init\_upstream,要么被设定为ngx\_http\_upstream\_init\_round\_robin,取决于当前upstream是否存在父模块。每个upstream模块都有自己的初始化接口,根据模块的继承关系,先初始化父模块,再初始化当前模块。往上追溯继承关系的源头,根模块为round robin。 413 | 414 | static char * 415 | ngx_http_upstream_keepalive(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 416 | { 417 | ngx_http_upstream_srv_conf_t *uscf; 418 | ngx_http_upstream_keepalive_srv_conf_t *kcf = conf; 419 | ...... 420 | /* init upstream handler */ 421 | uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); 422 | kcf->original_init_upstream = uscf->peer.init_upstream 423 | ? uscf->peer.init_upstream 424 | : ngx_http_upstream_init_round_robin; 425 | uscf->peer.init_upstream = ngx_http_upstream_init_keepalive; 426 | return NGX_CONF_OK; 427 | } 428 | 429 | 检视ngx\_http\_upstream\_init\_round\_robin初始化的实现,其将upstream配置的服务器解析后的地址结果(请读码,一个服务器可能解析出多个地址)形成数组(peer数组,元素数据类型为ngx\_http\_upstream\_rr\_peer\_t),最终放在了upstream配置下面(us->peer.data = peers)。 430 | 431 | ngx_int_t 432 | ngx_http_upstream_init_round_robin(ngx_conf_t *cf, 433 | ngx_http_upstream_srv_conf_t *us) 434 | { 435 | ngx_http_upstream_rr_peer_t *peer, **peerp; 436 | ngx_http_upstream_rr_peers_t *peers, *backup; 437 | ...... 438 | us->peer.data = peers; 439 | /* implicitly defined upstream has no backup servers */ 440 | return NGX_OK; 441 | } 442 | 443 | 那么,获取peer是直接通过ngx\_http\_upstream\_get\_round\_robin\_peer接口获取的吗?非也! 444 | 445 | 虽然upstream模块都继承于round robin,但是各自算法不同、策略不同,通常的办法是设定自己的peer数据结构,并继承ngx\_http\_upstream\_rr\_peer\_t结构,指向round robin模块的peer,这在upstream模块初始化的后半程完成(前半程为round robin模块初始化)。因此,获取peer需要在运行期间,根据算法策略获得相应的模块,并调用其peer get接口。 446 | 447 | 这里不可理喻之处在于,每个upstream单独维护了peer的down标志。比如,如果一个server同时配置在两个upstream中,运行期间其中一个upstream将其标down,另外一个upstream是不可感知的。 448 | 449 | 以consistent hash upstream模块为例,代码如下: 450 | 451 | typedef struct { 452 | u_char down; 453 | uint32_t hash; 454 | ngx_uint_t index; 455 | ngx_uint_t rnindex; 456 | ngx_http_upstream_rr_peer_t *peer; 457 | } ngx_http_upstream_chash_server_t; 458 | 459 | static ngx_int_t 460 | ngx_http_upstream_init_chash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) 461 | { 462 | u_char hash_buf[256]; 463 | ngx_int_t j, weight; 464 | ngx_uint_t sid, id, hash_len; 465 | ngx_uint_t i, n, *number, rnindex; 466 | ngx_http_upstream_rr_peer_t *peer; 467 | ngx_http_upstream_rr_peers_t *peers; 468 | ngx_http_upstream_chash_server_t *server; 469 | ngx_http_upstream_chash_srv_conf_t *ucscf; 470 | 471 | if (ngx_http_upstream_init_round_robin(cf, us) == NGX_ERROR) { 472 | return NGX_ERROR; 473 | } 474 | ucscf = ngx_http_conf_upstream_srv_conf(us, 475 | ngx_http_upstream_consistent_hash_module); 476 | if (ucscf == NULL) { 477 | return NGX_ERROR; 478 | } 479 | us->peer.init = ngx_http_upstream_init_chash_peer; 480 | peers = (ngx_http_upstream_rr_peers_t *) us->peer.data; 481 | if (peers == NULL) { 482 | return NGX_ERROR; 483 | } 484 | 485 | ...... 486 | 487 | ucscf->servers = ngx_pcalloc(cf->pool, 488 | (ucscf->number + 1) * 489 | sizeof(ngx_http_upstream_chash_server_t)); 490 | ...... 491 | ucscf->number = 0; 492 | for (i = 0; i < n; i++) { 493 | ...... 494 | for (j = 0; j < weight; j++) { 495 | server = &ucscf->servers[++ucscf->number]; 496 | server->peer = peer; 497 | server->rnindex = i; 498 | id = sid * 256 * 16 + j; 499 | server->hash = ngx_murmur_hash2((u_char *) (&id), 4); 500 | } 501 | } 502 | ...... 503 | return NGX_OK; 504 | } 505 | 506 | 获取peer的接口为ngx\_http\_upstream\_get\_chash\_peer,最关键的是获得了peer的sockaddr信息。 507 | 508 | static ngx_int_t 509 | ngx_http_upstream_get_chash_peer(ngx_peer_connection_t *pc, void *data) 510 | { 511 | ngx_http_upstream_rr_peer_t *peer; 512 | ngx_http_upstream_chash_server_t *server; 513 | ngx_http_upstream_chash_srv_conf_t *ucscf; 514 | 515 | ucscf = uchpd->ucscf; 516 | ...... 517 | 518 | peer = server->peer; 519 | pc->name = &peer->name; 520 | pc->sockaddr = peer->sockaddr; 521 | pc->socklen = peer->socklen; 522 | return NGX_OK; 523 | } 524 | 525 | 这正是我们想要的。 526 | 527 | ### 2.3、原理总结 528 | 529 | upstream系列模块根植于round robin模块,采用继承方式扩展能力边界。 530 | 531 | 初始化:首先通过round robin初始化完成前半程初始化,然后根据当前upstream的算法和策略,完成后半程初始化。后半程初始化继承round robin的peer数组,并重新组织。 532 | 533 | 获取peer:从当前upstream模块的peer get接口中获取。 534 | 535 | 建连:在content phase阶段,原生proxy模块介入,调用upstream模块的初始化(ngx\_http\_upstream\_init),后者发起建连。建连之前调用peer get获取peer的sockaddr信息。 536 | 537 | ## 3、xcache粘合upstream 538 | 539 | 粘合步骤如下: 540 | 541 | (1)增加一个新的指令upstream_by_bgn,用来替换proxy_pass指令。 542 | 543 | (2)根据配置指定的upstream名称,从全局的upstream列表(r->upstream)中,定位到upstream的配置uscf,对应的就是配置指向的upstream模块 544 | 545 | (3)调用uscf模块的peer init接口,完成peer get接口的设定 546 | 547 | (4)调用peer get接口,获取peer,从peer sockaddr中反向提取IP地址和端口号信息 548 | -------------------------------------------------------------------------------- /协程基础.md: -------------------------------------------------------------------------------- 1 | # 协程基础 2 | 3 | ## 1、背景原理 4 | 5 | 协程(coroutine)可以认为是一种用户态的线程(thread),与系统提供的线程不同的是,协程可以主动让出CPU时间,而不是由系统进行调度,即控制权在程序员手上。 6 | 7 | 因为可以看作是用户态线程,协程存在多种实现方式,没有一定之规。最简单的实现方式是,利用setjmp和longjmp实现协程的切换和恢复。 8 | 9 | 早期的协程实现,比如coco,广泛应用于游戏领域。Lua语言也实现了一个协程版本。 10 | 11 | 通常,协程拥有独立的调用堆栈(call stack),用来保存函数的返回地址、参数表、局部变量等信息。 12 | 13 | 协程拥有独立调用堆栈的好处在于,可以挂起协程。 14 | 15 | 切换前保存当前协程的运行状态,比如,堆栈顶寄存器(rsp),堆栈地寄存器(rbp),下一条待执行指令地址(rip);恢复(切换回来)就是根据此前保存的协程运行状态信息,更新相应的寄存器,完成切换(姑且称之为加载协程运行状态)。这里隐喻着,切换和恢复一对操作其实可以看作是同一个操作:保存当前协程的运行状态,加载目标协程的运行状态。当然,还存在两种情形:第一种,切换出去后不打算回来的,此时无需保存当前协程的运行状态;第二种,创建一个新的协程,此时目标协程没有运行状态,需要构建场景。 16 | 17 | 协程的运行状态称之为上下文(context)。 18 | 19 | Lua协程(Lua Coroutine)依赖Lua状态,不同的Lua状态各自完全独立,不共享任何数据。 20 | 21 | Lua状态有两种创建方式:一种是由luaL\_newstate接口创建,不拥有独立的调用堆栈;另一种是由lua\_newthread接口创建,拥有独立的调用堆栈。 22 | 23 | > 注意:lua\_newthread接口又称为Lua线程创建接口,Lua线程并非系统提供的线程,两者不要混淆。 24 | 25 | 在nginx lua模块中,由luaL\_newstate接口创建进程中唯一一个Lua状态,称之为Lua虚拟机(Lua VM)。在一个http请求的某个Lua介入的阶段(nginx phase),由lua\_newthread接口创建Lua状态。 26 | 27 | 在nginx的运行过程中,协程的切换为,从Lua虚拟机切到Lua状态,然后从Lua状态切回到Lua虚拟机,如此反复,Lua状态之间不存在切换。这是典型的master-slave模型。 28 | 29 | coco在Lua 5.1中以补丁的形式扩展了Lua,使得用户可以在Lua脚本中直接使用协程coroutine。 30 | 31 | 从上面可以看到,在nginx lua模块中,作者章亦春通过Lua虚拟机自己撸了一个Lua协程调度器。协程调度器需要根据一定的策略,决定调度到哪个协程上执行。协程拥有调度器,才算完整,可以称之为协程库,比如,最著名的就是Go的前身,libtask库。 32 | 33 | 下文描述的是glibc提供的一组用于创建、保存、切换用户态执行上下文(ucontext,userlevel context)的API,可以看作是 setjmp/long_jmp的升级版。利用ucontext做为协程接口基础,再撸个调度器,就形成了BGN的协程库。 34 | 35 | 36 | ## 2、ucontext数据结构 37 | 38 | ucontext的核心数据结构为ucontext\_t,在ucontext.h中定义, 39 | 40 | /* Userlevel context. */ 41 | typedef struct ucontext 42 | { 43 | unsigned long int uc_flags; 44 | struct ucontext *uc_link; 45 | stack_t uc_stack; 46 | mcontext_t uc_mcontext; 47 | __sigset_t uc_sigmask; 48 | struct _libc_fpstate __fpregs_mem; 49 | } ucontext_t; 50 | 51 | 52 | 53 | 其中, 54 | 55 | uc\_flags:初始置零。用来标记上下文是否由getcontext接口获得。用户不用关心此标。 56 | 57 | uc\_link:下一跳。当前上下文执行完成后,如果下一跳非空,则自动跳入下一跳执行;否则,进程或线程退出。 58 | 59 | uc\_stack:当前上下文的调用堆栈。 60 | 61 | uc\_mcontext:在从当前上下文切出(swap)时,保存当前的运行态的寄存器信息,即保存运行现场。 62 | 63 | uc\_sigmask:当前上下文的信号屏蔽集。建议置空,不在协程中处理信号。 64 | 65 | \_\_fpregs_mem:在从当前上下文切出(swap)时,保存当前的运行态的浮点寄存器信息(协处理器),即保存运行现场。 66 | 67 | 不同的硬件体系、操作系统,寄存器不同,需要保存运行现场的信息不同,所幸glibc屏蔽掉了这些差异。 68 | 69 | ## 3、ucontext接口 70 | 71 | ucontext在ucontext.h中定义了四个接口: 72 | 73 | void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); 74 | int getcontext(ucontext_t *ucp); 75 | int setcontext(const ucontext_t *ucp); 76 | int swapcontext(ucontext_t *oucp, ucontext_t *ucp); 77 | 78 | 其中, 79 | 80 | makecontext: 81 | 82 | 初始化上下文。函数指针func指明了该上下文的入口函数,argc指明入口参数个数,该值是可变的,参数紧随argc传入。 83 | 84 | 另外,在调用之前,还需要显式的指明其初始调用堆栈信息和运行时的信号屏蔽掩码(signal mask)。 同时也可以指定uc\_link字段,这样在func函数返回后,就会切换到uc\_link指向的上下文继续执行。 85 | 86 | > 请注意:在glibc-2.5中,入口函数的每个参数类型都是int型,这是一个bug,一直到glibc-2.18才算彻底修复,历经波折。BGN提供了一个补丁,手动压入参数到寄存器和堆栈中,解除glibc-2.18之前版本中的bug,但该方法仅适用于x86-64体系。 87 | 88 | getcontext: 89 | 90 | 获取当前运行现场并保存到上下文中。若后续调用setcontext或swapcontext恢复该上下文,则程序会沿着getcontext调用点之后继续执行,看起来好像刚从getcontext函数返回一样。 91 | 92 | setcontext: 93 | 94 | 切换到指定的上下文运行。在执行正确的情况下,该接口直接切入到新的上下文运行,不再返回。 95 | 96 | swapcontext: 97 | 98 | 保存当前运行现场到一个上下文(第一个参数oucp)中,然后切换到指定的上下文(第二个参数ucp)运行。 99 | 100 | ## 4、ucontext例子 101 | 102 | ### 4.1、 例:getcontext和setcontext实现无限循环 103 | 104 | #include 105 | #include 106 | #include 107 | #include 108 | int main(int argc, char **argv) 109 | { 110 | ucontext_t context; 111 | getcontext(&context); 112 | puts("Hello world"); 113 | sleep(1); 114 | setcontext(&context); 115 | return 0; 116 | } 117 | 118 | getcontext获取当前运行现场并保存到上下文context,setcontext切换回到前面保存的上下文context,从getcontext的下一条指令(这里就是下一条语句)接着运行,从而实现无限循环。本例的切换没有发生调用堆栈的切换,还是在进程的调用堆栈上进行,行为类似jmp指令、goto语句。 119 | 120 | 输出: 121 | 122 | Hello world 123 | Hello world 124 | Hello world 125 | Hello world 126 | Hello world 127 | Hello world 128 | ...... 129 | 130 | 131 | ### 4.2、 例:makecontext的陷阱 132 | 133 | #include 134 | #include 135 | #include 136 | #include 137 | void func(void) 138 | { 139 | puts("Hello World"); 140 | } 141 | int main(int argc, char **argv) 142 | { 143 | ucontext_t context; 144 | //getcontext(&context); 145 | context.uc_stack.ss_sp = malloc(64 * 1024); 146 | context.uc_stack.ss_size = 64 * 1024; 147 | context.uc_link = NULL; 148 | makecontext(&context, (void (*)(void))func, 0); 149 | setcontext(&context); 150 | return 0; 151 | } 152 | 153 | 运行输出: 154 | 155 | Segmentation fault (core dumped) 156 | 157 | 用gdb汇编跟踪, 158 | 159 | (gdb) si 160 | 50 in ../sysdeps/unix/sysv/linux/x86_64/setcontext.S 161 | 1: x/10i $pc 162 | => 0x7ffff7a7bb73 : jae 0x7ffff7a7bbd0 163 | 0x7ffff7a7bb75 : mov 0xe0(%rdi),%rcx 164 | 0x7ffff7a7bb7c : fldenv (%rcx) 165 | 0x7ffff7a7bb7e : ldmxcsr 0x1c0(%rdi) 166 | 0x7ffff7a7bb85 : mov 0xa0(%rdi),%rsp 167 | 0x7ffff7a7bb8c : mov 0x80(%rdi),%rbx 168 | 0x7ffff7a7bb93 : mov 0x78(%rdi),%rbp 169 | 0x7ffff7a7bb97 : mov 0x48(%rdi),%r12 170 | 0x7ffff7a7bb9b : mov 0x50(%rdi),%r13 171 | 0x7ffff7a7bb9f : mov 0x58(%rdi),%r14 172 | 173 | (gdb) si 174 | 55 in ../sysdeps/unix/sysv/linux/x86_64/setcontext.S 175 | 1: x/10i $pc 176 | => 0x7ffff7a7bb7c : fldenv (%rcx) 177 | 0x7ffff7a7bb7e : ldmxcsr 0x1c0(%rdi) 178 | 0x7ffff7a7bb85 : mov 0xa0(%rdi),%rsp 179 | 0x7ffff7a7bb8c : mov 0x80(%rdi),%rbx 180 | 0x7ffff7a7bb93 : mov 0x78(%rdi),%rbp 181 | 0x7ffff7a7bb97 : mov 0x48(%rdi),%r12 182 | 0x7ffff7a7bb9b : mov 0x50(%rdi),%r13 183 | 0x7ffff7a7bb9f : mov 0x58(%rdi),%r14 184 | 0x7ffff7a7bba3 : mov 0x60(%rdi),%r15 185 | 0x7ffff7a7bba7 : mov 0xa8(%rdi),%rcx 186 | 187 | (gdb) i r rcx 188 | rcx 0x0 0 189 | 190 | 可见,fldenv指令试图从零地址(寄存器rcx为0)恢复FPU(浮点处理器,协处理器)环境,即试图从零地址的内存中,恢复数据到浮点寄存器中,引起段错误。 191 | 192 | 从glibc的setcontext.S汇编中,查看setcontext实现部分,印证gdb跟踪结果: 193 | 194 | 195 | 196 | /* Restore the floating-point context. Not the registers, only the 197 | rest. */ 198 | movq oFPREGS(%rdi), %rcx 199 | fldenv (%rcx) 200 | ldmxcsr oMXCSR(%rdi) 201 | 202 | 再检查glibc的makecontext.c中\_\_makecontext的实现可见,makecontext主要是将入口函数地址和参数保存到上下文寄存器相关的内存中:入口地址放到RIP,堆栈地址放到RSP,堆栈顶置为\_\_start\_context(glibc的\_\_start\_context放在栈顶用来捕获makecontent的上下文没有下一跳的情形,确保进程或线程终止,或者加载下一跳,构建下一跳的调用场景)、上下文的下一跳放到堆栈合适的位置(由入口函数的参数个数而定),参数放到RDI,RSI,RDX,RCX,R8,R9辅助寄存器中,超过6个参数时,后面的参数压入堆栈中。整个过程没有浮点寄存器啥事,但是setcontext运行又需要从内存恢复浮点寄存器,怎么办? 203 | 204 | 浮点寄存器(或者说协处理器环境)可以和内存关联(个人理解类似mmap),可以在寄存器和内存之间建立联系(link),由fnstenv/fldenv指令完成换出、换入。 205 | 206 | 由于getcontext接口可以完成浮点寄存器到内存的换出,因此,利用其trick一下: 207 | 208 | 209 | #include 210 | #include 211 | #include 212 | #include 213 | void func(void) 214 | { 215 | puts("Hello World"); 216 | } 217 | int main(int argc, char **argv) 218 | { 219 | ucontext_t context; 220 | getcontext(&context); 221 | context.uc_stack.ss_sp = malloc(64 * 1024); 222 | context.uc_stack.ss_size = 64 * 1024; 223 | context.uc_link = NULL; 224 | makecontext(&context, (void (*)(void))func, 0); 225 | setcontext(&context); 226 | return 0; 227 | } 228 | 229 | 输出: 230 | 231 | Hello World 232 | 233 | 结果正确。整个处理手法不那么光明正大。 234 | 235 | ### 4.3、例: makecontext和swapcontext 236 | 237 | #include 238 | #include 239 | #include 240 | #include 241 | #include 242 | void func(void) 243 | { 244 | puts("Hello World"); 245 | } 246 | int main(int argc, char **argv) 247 | { 248 | ucontext_t master, slave; 249 | getcontext(&slave); 250 | slave.uc_stack.ss_sp = malloc(64 * 1024); 251 | slave.uc_stack.ss_size = 64 * 1024; 252 | slave.uc_link = NULL; 253 | makecontext(&slave, (void (*)(void))func, 0); 254 | swapcontext(&master, &slave); 255 | puts("Hello BGN"); 256 | return 0; 257 | } 258 | 259 | 将主进程作为主上下文(master),构建从上下文(slave),通过swapcontext切换上下文,输出: 260 | 261 | Hello World 262 | 263 | 同样地,如果不事先通过getcontext在从上下文(slave)中保存浮点寄存器,切换会发生段错误。 264 | 265 | 那么,为什么没有输出”Hello BGN“呢?这是因为,slave的下一跳为空,执行完入口函数后,就进入glibc的\_\_start\_context执行退出了: 266 | 267 | 2: x/20i $pc 268 | => 0x7ffff7a7e014 <__start_context+20>: callq 0x7ffff7a6f980 <__GI_exit> 269 | 0x7ffff7a7e019 <__start_context+25>: hlt 270 | 0x7ffff7a7e01a: nopw 0x0(%rax,%rax,1) 271 | 0x7ffff7a7e020 : test %rdi,%rdi 272 | 0x7ffff7a7e023 : lea 0x11dc59(%rip),%rax # 0x7ffff7b9bc83 273 | 0x7ffff7a7e02a : je 0x7ffff7a7e040 274 | 0x7ffff7a7e02c : movabs $0x7974742f7665642f,%rax 275 | 0x7ffff7a7e036 : movb $0x0,0x8(%rdi) 276 | 0x7ffff7a7e03a : mov %rax,(%rdi) 277 | ---Type to continue, or q to quit---q 278 | Quit 279 | (gdb) where 280 | #0 __start_context () at ../sysdeps/unix/sysv/linux/x86_64/__start_context.S:45 281 | #1 0x0000000000000000 in ?? () 282 | 283 | 设定slave的下一跳为master,我们再看: 284 | 285 | #include 286 | #include 287 | #include 288 | #include 289 | #include 290 | void func(void) 291 | { 292 | puts("Hello World"); 293 | } 294 | int main(int argc, char **argv) 295 | { 296 | ucontext_t master, slave; 297 | getcontext(&slave); 298 | slave.uc_stack.ss_sp = malloc(64 * 1024); 299 | slave.uc_stack.ss_size = 64 * 1024; 300 | slave.uc_link = &master; 301 | makecontext(&slave, (void (*)(void))func, 0); 302 | swapcontext(&master, &slave); 303 | puts("Hello BGN"); 304 | return 0; 305 | } 306 | 307 | 输出: 308 | 309 | Hello World 310 | Hello BGN 311 | 312 | 结果正确。gdb查看堆栈, 313 | 314 | (gdb) n 315 | Hello World 316 | 23 puts("Hello BGN"); 317 | (gdb) where 318 | #0 main (argc=1, argv=0x7fffffffe3e8) at test_003.c:23 319 | 320 | 正是master的调用堆栈,栈顶为main。 321 | 322 | 主上下文(master)在swapcontext接口中保存了当前运行现场,RIP指向swapcontext之后的语句(对应的指令)puts。所以,当slave执行完func后,根据slave堆栈压栈规则,弹出slave的下一跳,即master,开始执行,即执行puts输出Hello BGN。 323 | 324 | 个人认为,这里始终存在潜在的协处理器风险:利用getcontext保存当前浮点寄存器现场,由于它还保存其他寄存器现场,所以必须在makecontext之前调用,否则会覆盖makecontext设定的寄存器。如果makecontext和 325 | setcontext/swapcontext相距过远,中间存在改变浮点寄存器的话,切换到目标上下文时,从内存中恢复的浮点寄存器将不符合预期。 326 | 327 | ### 4.4、例: makecontext和setcontext跨调用栈 328 | 329 | #include 330 | #include 331 | #include 332 | #include 333 | #include 334 | void func(void) 335 | { 336 | puts("Hello World"); 337 | } 338 | int main(int argc, char **argv) 339 | { 340 | ucontext_t master, slave; 341 | getcontext(&slave); 342 | slave.uc_stack.ss_sp = malloc(64 * 1024); 343 | slave.uc_stack.ss_size = 64 * 1024; 344 | slave.uc_link = &master; 345 | makecontext(&slave, (void (*)(void))func, 0); 346 | 347 | getcontext(&master); 348 | setcontext(&slave); 349 | puts("Hello BGN"); 350 | return 0; 351 | } 352 | 353 | 设定slave的下一跳为master,在setcontext之前保存现场到master中,然后用setcontext切换,输出: 354 | 355 | Hello World 356 | Hello World 357 | 358 | setcontext切出后是不会返回的,所以后面的Hello BGN不会被输出。这里把slave的下一跳设置成master,slave执行完后由回到了getcontext的下一条语句执行,即再次执行setcontext,再次进入slave执行。 359 | 360 | 按这个逻辑,master到slave到master可以无限循环下去,那么为什么只看到两条Hello World的输出后就结束了呢? 361 | 362 | 根源就是glibc的\_\_start\_context。在调用makecontext时,makecontext在slave的堆栈上塞入了\_\_start\_context,当slave的入口函数执行完毕时,就进入了\_\_start\_context,它将slave的下一跳的现场恢复执行,回到master的调用堆栈执行,此时slave的调用堆栈中,\_\_start\_context在刚才执行前,已被弹出,回到master调用堆栈时,slave的堆栈为空。回到master的调用堆栈后,执行到setcontext时, setcontext构建slave的调用场景,再次执行入口函数后slave的堆栈为空,退出。 363 | 364 | 也就是说,上下文的下一跳能够被执行,完全是glibc的\_\_start\_context的功劳:如果存在下一跳,当前入口函数执行完毕后,\_\_start\_context加载一下条的上下文,构建调用场景,发起跳转。如果下一跳为空,\_\_start_context则调用退出函数,退出进程或线程。 365 | 366 | 根据上面的分析,在setcontext前调用makecontext,确保slave的堆栈中压入\_\_start\_context,即可实现无限循环。调整代码: 367 | 368 | #include 369 | #include 370 | #include 371 | #include 372 | #include 373 | void func(void) 374 | { 375 | puts("Hello World"); 376 | } 377 | int main(int argc, char **argv) 378 | { 379 | ucontext_t master, slave; 380 | getcontext(&slave); 381 | slave.uc_stack.ss_sp = malloc(64 * 1024); 382 | slave.uc_stack.ss_size = 64 * 1024; 383 | slave.uc_link = &master; 384 | 385 | getcontext(&master); 386 | makecontext(&slave, (void (*)(void))func, 0); 387 | setcontext(&slave); 388 | puts("Hello BGN"); 389 | return 0; 390 | } 391 | 392 | 输出: 393 | 394 | Hello world 395 | Hello world 396 | Hello world 397 | Hello world 398 | Hello world 399 | Hello world 400 | ...... 401 | 402 | 结果正确。 403 | 404 | ### 4.5、例:在目标堆栈中setcontext 405 | 406 | #include 407 | #include 408 | #include 409 | #include 410 | #include 411 | void func(void *slave) 412 | { 413 | puts("Hello World"); 414 | setcontext(slave); 415 | } 416 | int main(int argc, char **argv) 417 | { 418 | ucontext_t slave; 419 | getcontext(&slave); 420 | slave.uc_stack.ss_sp = malloc(64 * 1024); 421 | slave.uc_stack.ss_size = 64 * 1024; 422 | slave.uc_link = NULL; 423 | 424 | makecontext(&slave, (void (*)(void))func, 1, (void *)&slave); 425 | setcontext(&slave); 426 | puts("Hello BGN"); 427 | return 0; 428 | } 429 | 430 | 在主进程中,设定slave的下一跳为空。切换到slave后,通过setcontext重新构建入口函数调用场景。由于setcontext不返回,所以入口函数永不结束返回,即堆栈顶部压入的\_\_start\_context没有机会被执行,从而无限循环下去。 431 | 432 | 输出: 433 | 434 | 435 | Hello world 436 | Hello world 437 | Hello world 438 | Hello world 439 | Hello world 440 | Hello world 441 | ...... 442 | 443 | 结果正确,但是此方法破坏性极大,因为setcontext是切换到当前的slave调用堆栈,恢复slave原始的在main中构建的上下文,也就是说当前slave堆栈被清空重置了。建议慎用。 444 | 445 | ### 4.6、例:在目标堆栈中makecontext 446 | 447 | #include 448 | #include 449 | #include 450 | #include 451 | #include 452 | void func(void *slave); 453 | void func2(void *slave); 454 | void func2(void *slave) 455 | { 456 | puts("Hello China"); 457 | makecontext(slave, (void (*)(void))func, 1, (void *)slave); 458 | setcontext(slave); 459 | } 460 | void func(void *slave) 461 | { 462 | puts("Hello World"); 463 | makecontext(slave, (void (*)(void))func2, 1, (void *)slave); 464 | setcontext(slave); 465 | } 466 | int main(int argc, char **argv) 467 | { 468 | ucontext_t slave; 469 | getcontext(&slave); 470 | slave.uc_stack.ss_sp = malloc(64 * 1024); 471 | slave.uc_stack.ss_size = 64 * 1024; 472 | slave.uc_link = NULL; 473 | 474 | makecontext(&slave, (void (*)(void))func, 1, (void *)&slave); 475 | setcontext(&slave); 476 | puts("Hello BGN"); 477 | return 0; 478 | } 479 | 480 | 在主进程中,设定slave的下一跳为空。切换到slave时,通过setcontext重新构建入口函数func调用场景。进入slave的func后,重新构建入口函数为func2的上下文,在func2中重新构建入口函数为func的上下文。 481 | 482 | 输出: 483 | 484 | Hello World 485 | Hello China 486 | Hello World 487 | Hello China 488 | Hello World 489 | Hello China 490 | ...... 491 | 492 | 成功在func和func2之间来回切换,但是切换发生在同一个目标堆栈上,破坏性同样很大。 493 | 494 | ### 4.7、例:在不同目标堆栈中swapcontext 495 | 496 | #include 497 | #include 498 | #include 499 | #include 500 | #include 501 | typedef struct 502 | { 503 | ucontext_t *slave1; 504 | ucontext_t *slave2; 505 | }node_t; 506 | void func1(node_t *node); 507 | void func2(node_t *node); 508 | void func2(node_t *node) 509 | { 510 | puts("Hello slave2"); 511 | swapcontext(node->slave2, node->slave1); 512 | } 513 | void func1(node_t *node) 514 | { 515 | puts("Hello slave1"); 516 | swapcontext(node->slave1, node->slave2); 517 | } 518 | int main(int argc, char **argv) 519 | { 520 | ucontext_t slave1, slave2; 521 | node_t node; 522 | node.slave1 = &slave1; 523 | node.slave2 = &slave2; 524 | getcontext(&slave1); 525 | slave1.uc_stack.ss_sp = malloc(64 * 1024); 526 | slave1.uc_stack.ss_size = 64 * 1024; 527 | slave1.uc_link = NULL; 528 | makecontext(&slave1, (void (*)(void))func1, 1, (void *)&node); 529 | getcontext(&slave2); 530 | slave2.uc_stack.ss_sp = malloc(64 * 1024); 531 | slave2.uc_stack.ss_size = 64 * 1024; 532 | slave2.uc_link = NULL; 533 | makecontext(&slave2, (void (*)(void))func2, 1, (void *)&node); 534 | 535 | setcontext(&slave1); 536 | puts("Hello BGN"); 537 | return 0; 538 | } 539 | 540 | 构建两个上下文slave1和slave2,入口函数分别为func1和func2,通过setcontext切换到slave1,不返回。 541 | 542 | slave1中通过swapcontext切换到slave2,slave2中通过swapcontext切换到slave1。 543 | 544 | 在第一次执行slave1的入口函数func1时,swapcontext将当前运行现场保存到slave1中,RIP指向下一条语句(指令),隐式地为return;切换到slave2后,swapcontext切换回slave1,执行return,进入glibc的\_\_start\_context,发现当前slave1没有下一跳,所以执行退出,结束本进程,不可能切回到slave2。 545 | 546 | 输出: 547 | 548 | Hello slave1 549 | Hello slave2 550 | 551 | 上述逻辑,等价于将slave1的下一跳指向slave2,即 552 | 553 | #include 554 | #include 555 | #include 556 | #include 557 | #include 558 | void func2(void) 559 | { 560 | puts("Hello slave2"); 561 | } 562 | void func1(void) 563 | { 564 | puts("Hello slave1"); 565 | } 566 | int main(int argc, char **argv) 567 | { 568 | ucontext_t slave1, slave2; 569 | getcontext(&slave1); 570 | slave1.uc_stack.ss_sp = malloc(64 * 1024); 571 | slave1.uc_stack.ss_size = 64 * 1024; 572 | slave1.uc_link = &slave2; 573 | makecontext(&slave1, (void (*)(void))func1, 0); 574 | getcontext(&slave2); 575 | slave2.uc_stack.ss_sp = malloc(64 * 1024); 576 | slave2.uc_stack.ss_size = 64 * 1024; 577 | slave2.uc_link = NULL; 578 | makecontext(&slave2, (void (*)(void))func2, 0); 579 | 580 | setcontext(&slave1); 581 | puts("Hello BGN"); 582 | return 0; 583 | } 584 | 585 | 结果完全一致。 586 | 587 | ### 4.8、例:master-slave调度模型 588 | 589 | #include 590 | #include 591 | #include 592 | #include 593 | #include 594 | void func2(void) 595 | { 596 | puts("Hello slave2"); 597 | } 598 | void func1(void) 599 | { 600 | puts("Hello slave1"); 601 | } 602 | int main(int argc, char **argv) 603 | { 604 | ucontext_t master, slave1, slave2; 605 | getcontext(&slave1); 606 | slave1.uc_stack.ss_sp = malloc(64 * 1024); 607 | slave1.uc_stack.ss_size = 64 * 1024; 608 | slave1.uc_link = &master; 609 | makecontext(&slave1, (void (*)(void))func1, 0); 610 | getcontext(&slave2); 611 | slave2.uc_stack.ss_sp = malloc(64 * 1024); 612 | slave2.uc_stack.ss_size = 64 * 1024; 613 | slave2.uc_link = &master; 614 | makecontext(&slave2, (void (*)(void))func2, 0); 615 | swapcontext(&master, &slave1); 616 | puts("Hello BGN"); 617 | swapcontext(&master, &slave2); 618 | puts("Hello BGN"); 619 | return 0; 620 | } 621 | 622 | 准备一个master上下文和两个slave上下文。两个slave的下一跳都指向master。 623 | 624 | 遇到第一个swapcontext切换到slave1,当前现场保存到master中;slave1堆栈中压入的\_\_start\_context在入口函数func1执行完毕后,恢复下一跳的调用现场,即回到第一个swapcontext的下一条语句开始执行。 625 | 626 | 遇到第二个swapcontext切换到slave2,当前现场保存到master中;slave2堆栈中压入的\_\_start\_context在入口函数func2执行完毕后,恢复下一跳的调用现场,即回到第二个swapcontext的下一条语句开始执行。 627 | 628 | 输出: 629 | 630 | Hello slave1 631 | Hello BGN 632 | Hello slave2 633 | Hello BGN 634 | 635 | 结果正确。这是基本的master-slave调度模型。 636 | 637 | 来看看协程的挂起例子。 638 | 639 | #include 640 | #include 641 | #include 642 | #include 643 | #include 644 | typedef struct 645 | { 646 | ucontext_t *master; 647 | ucontext_t *slave; 648 | }node_t; 649 | void func2(void) 650 | { 651 | puts("Hello slave2"); 652 | } 653 | void func1(node_t *node) 654 | { 655 | puts("Hello slave1"); 656 | swapcontext(node->slave, node->master); 657 | puts("Hello slave1 again"); 658 | } 659 | int main(int argc, char **argv) 660 | { 661 | ucontext_t master, slave1, slave2; 662 | node_t node; 663 | node.master = &master; 664 | node.slave = &slave1; 665 | getcontext(&slave1); 666 | slave1.uc_stack.ss_sp = malloc(64 * 1024); 667 | slave1.uc_stack.ss_size = 64 * 1024; 668 | slave1.uc_link = &master; 669 | makecontext(&slave1, (void (*)(void))func1, 1, (void *)&node); 670 | getcontext(&slave2); 671 | slave2.uc_stack.ss_sp = malloc(64 * 1024); 672 | slave2.uc_stack.ss_size = 64 * 1024; 673 | slave2.uc_link = &master; 674 | makecontext(&slave2, (void (*)(void))func2, 0); 675 | swapcontext(&master, &slave1); 676 | puts("Hello BGN"); 677 | swapcontext(&master, &slave2); 678 | puts("Hello BGN"); 679 | swapcontext(&master, &slave1); 680 | puts("Hello BGN"); 681 | return 0; 682 | } 683 | 684 | master切换到slave1执行,遇到func1中的swapcontext后,切回到master;master切换到slave2执行完成,下一跳是master,返回master;master又切回到slave1,从func1的swapcontext下一条执行到func1完成,下一跳是master,返回master。 685 | 686 | 输出: 687 | 688 | Hello slave1 689 | Hello BGN 690 | Hello slave2 691 | Hello BGN 692 | Hello slave1 again 693 | Hello BGN 694 | 695 | 结果正确。 696 | --------------------------------------------------------------------------------