Lanyon
2025-09-11T01:26:44+00:00
https://dongma.github.io
Sam Ma
sam_newyork@163.com
谈谈我所了解的前端技术的发展
2025-04-22T00:00:00+00:00
https://dongma.github.io/2025/04/22/谈谈我所了解的前端技术的发展
<h3 id="为什么会突然提到这个话题">为什么会突然提到这个话题?</h3>
<p>最近看到一个很好看的小程序前端模版<code class="language-plaintext highlighter-rouge">nxdc-milktea</code>,展示效果如下图所示,想着看把程序跑起来。看了前端代码仓库中有<code class="language-plaintext highlighter-rouge">App.vue</code>,我理解应该是基于<code class="language-plaintext highlighter-rouge">vue</code>开发的项目,但此项目没有<code class="language-plaintext highlighter-rouge">package.json</code>,按<code class="language-plaintext highlighter-rouge">vue</code>的方式启动不了。</p>
<div>
<img src="../../../../resource/2025/wxmp/nxdc-milktea.jpg" width="680" />
</div>
<!-- more -->
<p>也给作者留过言”服务如何运行起来?”,因为项目开发的时间很久了,作者目前暂未答复。模版也作为插件发布在了<code class="language-plaintext highlighter-rouge">DCloud</code>的<code class="language-plaintext highlighter-rouge">uni-app</code>插件市场,看了<code class="language-plaintext highlighter-rouge">uni-app</code>的官方文档,一切都变得豁然开朗。首先需求下载<code class="language-plaintext highlighter-rouge">hbuilderx</code>插件,然后导入插件的内容,在左上角点击<code class="language-plaintext highlighter-rouge">发行->小程序(微信)</code>,会将导出的代码导入<code class="language-plaintext highlighter-rouge">微信开发工具</code>,如下图所示。</p>
<div>
<img src="../../../../resource/2025/wxmp/hbuiderx_export.jpg" width="680" />
</div>
<p>小程序代码导入微信开发工具后的样子,左边是模拟器,右边是代码区。看起来有种开发<code class="language-plaintext highlighter-rouge">ios</code>或<code class="language-plaintext highlighter-rouge">android app</code>的感觉,相比而言,理解开发小程序的门槛能低一些。</p>
<div>
<img src="../../../../resource/2025/wxmp/weixin_tool.jpg" width="680" />
</div>
<h3 id="什么是uni-app">什么是<code class="language-plaintext highlighter-rouge">uni-app</code>?</h3>
<p>简单来说,<code class="language-plaintext highlighter-rouge">uni-app</code>我理解是一种移动端通用的开发框架,像做移动端应用时,基于<code class="language-plaintext highlighter-rouge">uni-app</code>只需开发一版,然后就可以导出针对不同平台的安装包,例如:安卓、<code class="language-plaintext highlighter-rouge">ios</code>、鸿蒙、微信小程序、支付宝小程序、京东小程序等,具体可以看<code class="language-plaintext highlighter-rouge">uni-app</code>的官方文档。</p>
<p>有一件有意思的事情,在<code class="language-plaintext highlighter-rouge">19</code>年时,公司安排组内同事去上海某大型国有银行出差支持,在上海呆了<code class="language-plaintext highlighter-rouge">1</code>个多月。在那边主要做应用后端,前台是一个数据探查的<code class="language-plaintext highlighter-rouge">app</code>,是基于<code class="language-plaintext highlighter-rouge">uni-app</code>开发,<code class="language-plaintext highlighter-rouge">1</code>个前端、<code class="language-plaintext highlighter-rouge">2</code>个后端差不多一个多左右完成开发上线。同时期,也有友商在现场开发类似应用,好几个<code class="language-plaintext highlighter-rouge">android</code>开发,一个月的开发速度惊讶到对方了,跑过来问同事,你们是怎么做的,怎么那么快😂?</p>
<p>还想挖同事过去,后来也是从同事那边知道这个事情,我想了下,肯定写安卓组件比写<code class="language-plaintext highlighter-rouge">html</code>慢,并且要同时适配安卓和<code class="language-plaintext highlighter-rouge">ios</code>,应该是这个原因。</p>
<p><code class="language-plaintext highlighter-rouge">uni-app</code>使用的是<code class="language-plaintext highlighter-rouge">vue</code>的语法,不是小程序自定义的语法,<code class="language-plaintext highlighter-rouge">DCloud</code>与<code class="language-plaintext highlighter-rouge">vue</code>合作,在<code class="language-plaintext highlighter-rouge">vue.js</code>官网提供了免费视频教程<code class="language-plaintext highlighter-rouge">https://learning.dcloud.io/#/</code> 。</p>
<h3 id="了解下vuejs的语法">了解下<code class="language-plaintext highlighter-rouge">vue.js</code>的语法</h3>
<p><code class="language-plaintext highlighter-rouge">vue.js</code>文档地址<code class="language-plaintext highlighter-rouge">https://v2.cn.vuejs.org/v2/guide/</code>, 和其它js库一样,在页面中引入<code class="language-plaintext highlighter-rouge">vue.js</code>也是通过<code class="language-plaintext highlighter-rouge"><script></code>标签引入的。</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- html中引入vue.js, 生产环境版本,优化了尺寸和速度 --></span>
<span class="nt"><script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/vue@2"</span><span class="nt">></script></span>
</code></pre></div></div>
<p>第一个<code class="language-plaintext highlighter-rouge">Vue</code>应用,引入<code class="language-plaintext highlighter-rouge">vue.js</code>后,在当前页面就回有一个<code class="language-plaintext highlighter-rouge">Vue</code>对象来绑定页面属性,其属性<code class="language-plaintext highlighter-rouge">el</code>就是页面选择器,选择了<code class="language-plaintext highlighter-rouge">id=app</code>的<code class="language-plaintext highlighter-rouge">div</code>,<code class="language-plaintext highlighter-rouge">dom</code>中的<code class="language-plaintext highlighter-rouge">message</code>就是取的<code class="language-plaintext highlighter-rouge">Vue#data</code>中的元素。</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- v-bind动态给class绑定变量,当前div是绑定color颜色; @click.stop能阻止事件向上传播 --></span>
<span class="nt"><div</span> <span class="na">id=</span><span class="s">"app"</span> <span class="na">v-bind:class=</span><span class="s">"color"</span> <span class="err">@</span><span class="na">click.stop=</span><span class="s">"click"</span><span class="nt">></span>
<span class="c"><!-- 两个大括号要连接起来,模版插值语法,加入v-once后,message属性更新后不会再刷新到页面 --></span>
<span class="nt"><span</span> <span class="na">v-once</span><span class="nt">></span>Message: { { message }}<span class="nt"></span></span>
<span class="c"><!-- v-html会将变量的值 直接以html展示 --></span>
<span class="nt"><p></span>using v-html directive: <span class="nt"><span</span> <span class="na">v-html=</span><span class="s">"rawHtml"</span><span class="nt">></span></p></span>
<span class="c"><!-- vue中的条件渲染,v-if会根据seen变量的值 来决定是否展示元素内容(为true时展示),v-bind绑定元素内容 --></span>
<span class="nt"><span</span> <span class="na">v-if=</span><span class="s">"seen"</span><span class="nt">></span>你现在看到我了<span class="nt"></span></span>
<span class="nt"><span</span> <span class="na">v-else</span><span class="nt">></span>Oh no 😂<span class="nt"></span></span>
<span class="c"><!-- vue中的列表渲染,v-for语法展示list中的元素,Foo和Bar元素各生成一个li元素 --></span>
<span class="nt"><ul></span>
<span class="nt"><li</span> <span class="na">v-for=</span><span class="s">"item in items"</span><span class="nt">></span>
{ {item.message}}
<span class="nt"></li></span>
<span class="nt"></ul></span>
<span class="c"><!-- v-on指令监听DOM事件,counter监听click事件,每多点击一次click,counter的值就会加1 --></span>
<span class="nt"><button</span> <span class="na">v-on:click=</span><span class="s">"counter +=1"</span><span class="nt">></span>Add 1<span class="nt"></button></span>
<span class="nt"></div></span>
</code></pre></div></div>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Vue</span><span class="p">({</span>
<span class="na">el</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#app</span><span class="dl">'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="p">{</span>
<span class="na">message</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Hello Vue!</span><span class="dl">'</span><span class="p">,</span>
<span class="na">a</span><span class="p">:</span> <span class="dl">"</span><span class="s2">normal value</span><span class="dl">"</span><span class="p">,</span>
<span class="na">rawHtml</span><span class="p">:</span> <span class="dl">'</span><span class="s1"><span style="color:red">this is should be red</span></span><span class="dl">'</span><span class="p">,</span>
<span class="na">color</span><span class="p">:</span> <span class="dl">'</span><span class="s1">red</span><span class="dl">'</span><span class="p">,</span>
<span class="na">seen</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">elems</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span><span class="na">message</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Foo</span><span class="dl">'</span><span class="p">},</span>
<span class="p">{</span><span class="na">message</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Bar</span><span class="dl">'</span><span class="p">}</span>
<span class="p">],</span>
<span class="na">counter</span><span class="p">:</span> <span class="mi">0</span>
<span class="p">},</span>
<span class="na">method</span><span class="p">:</span> <span class="p">{</span>
<span class="na">click</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">click element!</span><span class="dl">"</span><span class="p">)}</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="c1">// 可通$访问vue暴露的属性和方法,另一种vm.$watch('a', func(newValue, oldValue){xxx}) ,观察到a变化时,会触发回调用函数</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">$data</span><span class="p">.</span><span class="nx">message</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Change Value!</span><span class="dl">"</span>
</code></pre></div></div>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.red</span> <span class="p">{</span><span class="nl">color</span><span class="p">:</span><span class="no">red</span><span class="p">;}</span>
<span class="nc">.blue</span> <span class="p">{</span><span class="nl">color</span><span class="p">:</span><span class="no">blue</span><span class="p">;</span><span class="nl">font-size</span><span class="p">:</span><span class="m">100px</span><span class="p">;}</span>
</code></pre></div></div>
浅谈nginx服务代理
2025-04-07T00:00:00+00:00
https://dongma.github.io/2025/04/07/浅谈nginx服务代理
<h3 id="nginx用docker安装"><code class="language-plaintext highlighter-rouge">nginx</code>用<code class="language-plaintext highlighter-rouge">docker</code>安装</h3>
<p><code class="language-plaintext highlighter-rouge">nginx</code>服务安装,我是用<code class="language-plaintext highlighter-rouge">docker</code>安装的,<code class="language-plaintext highlighter-rouge">mac</code>系统编译<code class="language-plaintext highlighter-rouge">nginx</code>源码安装有点问题。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull nginx <span class="c"># 从docker仓库拉取nginx镜像</span>
<span class="c"># nginx目录挂载参考此文章,https://blog.csdn.net/baidu_21349635/article/details/102738972</span>
docker run <span class="nt">--name</span> nginx-0807 <span class="nt">-v</span> /Users/madong/software/nginx/nginx.conf:/etc/nginx/nginx.conf <span class="se">\</span>
<span class="nt">-v</span> /Users/madong/software/nginx/conf.d:/etc/nginx/conf.d <span class="se">\</span>
<span class="nt">-v</span> /Users/madong/software/nginx/html:/usr/share/nginx/html <span class="se">\</span>
<span class="nt">-v</span> /Users/madong/software/nginx/logs:/var/log/nginx <span class="nt">-p</span> 8080:80 <span class="nt">-d</span> nginx
<span class="c"># nginx服务验证,curl有返回html内容时,则表示nginx服务启动成功了; /etc/nginx存nginx配置、/usr/share/nginx存在html、/var/log/nginx/存放nginx的access_log</span>
curl <span class="s1">'http://localhost:8080/'</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">nginx</code>也支持热更新,当修改nginx配置后,可使用<code class="language-plaintext highlighter-rouge">nginx -s reload</code>使修改的配置生效,当<code class="language-plaintext highlighter-rouge">access.log</code>文件特别大时,可使用<code class="language-plaintext highlighter-rouge">nginx -s reopen</code>切割日志。
<!-- more --></p>
<h3 id="nginx用源码安装"><code class="language-plaintext highlighter-rouge">nginx</code>用源码安装</h3>
<p>使用<code class="language-plaintext highlighter-rouge">nginx</code>源码包安装,用<code class="language-plaintext highlighter-rouge">docker nginx</code>有个明显的问题,那就是<code class="language-plaintext highlighter-rouge">nginx.conf</code>中<code class="language-plaintext highlighter-rouge">listen</code>不同端口时,容器内端口向外映射很麻烦,所以用源代码装。
步骤可分为以下<code class="language-plaintext highlighter-rouge">4</code>步:</p>
<ol>
<li>从<code class="language-plaintext highlighter-rouge">nginx</code>官网下载<code class="language-plaintext highlighter-rouge">stable version</code>源码,https://nginx.org/en/download.html;</li>
<li>由于<code class="language-plaintext highlighter-rouge">nginx</code>有<code class="language-plaintext highlighter-rouge">c</code>的代码,所以需下载<code class="language-plaintext highlighter-rouge">pcre</code>、<code class="language-plaintext highlighter-rouge">openssl</code>和<code class="language-plaintext highlighter-rouge">zlib</code>工具,具体可参考文章:https://blog.csdn.net/a1004084857/article/details/128512612;</li>
<li>解压<code class="language-plaintext highlighter-rouge">nginx zip</code>包,进入解压路径,对<code class="language-plaintext highlighter-rouge">nginx</code>进行配置,<code class="language-plaintext highlighter-rouge">--prefix</code>指定编译后的<code class="language-plaintext highlighter-rouge">nginx</code>二进制文件存放目录,<code class="language-plaintext highlighter-rouge">--with-pcre|zlib|openssl</code>分别为工具的解压逻辑;
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./configure <span class="nt">--prefix</span><span class="o">=</span>/Users/madong/software/c_nginx_1.28.0 <span class="se">\</span>
<span class="nt">--with-http_ssl_module</span> <span class="se">\</span>
<span class="nt">--with-pcre</span><span class="o">=</span>./compile/pcre-8.45 <span class="se">\</span>
<span class="nt">--with-zlib</span><span class="o">=</span>./compile/zlib-1.3.1 <span class="se">\</span>
<span class="nt">--with-openssl</span><span class="o">=</span>./compile//openssl-3.0.7
</code></pre></div> </div>
</li>
<li>配置完成后,执行<code class="language-plaintext highlighter-rouge">make install</code>,则编译后的<code class="language-plaintext highlighter-rouge">nginx</code>就会出现在<code class="language-plaintext highlighter-rouge">/Users/madong/software/c_nginx_1.28.0</code>这个目录中;</li>
</ol>
<h3 id="nginx搭建一个静态资源web服务器"><code class="language-plaintext highlighter-rouge">nginx</code>搭建一个静态资源web服务器</h3>
<p><code class="language-plaintext highlighter-rouge">nginx</code>源码编译后,会生成<code class="language-plaintext highlighter-rouge">conf</code>、<code class="language-plaintext highlighter-rouge">html</code>、<code class="language-plaintext highlighter-rouge">logs</code>、<code class="language-plaintext highlighter-rouge">sbin</code>这几个目录,其分别是:<code class="language-plaintext highlighter-rouge">nginx</code>配置、<code class="language-plaintext highlighter-rouge">html</code>默认页面、<code class="language-plaintext highlighter-rouge">access_log</code>、启动脚本等,启动<code class="language-plaintext highlighter-rouge">nginx</code>的脚本:<code class="language-plaintext highlighter-rouge">./sbin/nginx</code>。</p>
<p>下方的内容是<code class="language-plaintext highlighter-rouge">nginx.cnf</code>配置的一部分,在<code class="language-plaintext highlighter-rouge">nginx</code>的安装目录下放了<code class="language-plaintext highlighter-rouge">neo4j</code>文档,在线文档对应路径为<code class="language-plaintext highlighter-rouge">neo4j</code>。配置解释,<code class="language-plaintext highlighter-rouge">nginx</code>监听<code class="language-plaintext highlighter-rouge">8093</code>端口,<code class="language-plaintext highlighter-rouge">location /</code>表示请求url为<code class="language-plaintext highlighter-rouge">ip:8093:/</code>时,进入此配置代码块。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server {
listen 8093;
location / {
# autoindex on;
alias neo4j/;
# set $limit_rate 1k; #限制nginx向浏览器发送流量的速度
# root html;
# index nothing; # 禁用inde文件
index index.html index.htm;
}
}
</code></pre></div></div>
<p>代码配置中有<code class="language-plaintext highlighter-rouge">root</code>和<code class="language-plaintext highlighter-rouge">alias</code>两个指令,建议用<code class="language-plaintext highlighter-rouge">alias</code>(一般用<code class="language-plaintext highlighter-rouge">root</code>配根目录,用<code class="language-plaintext highlighter-rouge">alias</code>配置一般的路径),打开浏览器,输入<code class="language-plaintext highlighter-rouge">http://127.0.0.1:8093/</code> 就可以访问到静态资源文档,如果想限制浏览器下载资源速度,可设置<code class="language-plaintext highlighter-rouge">set $limit_rate 1k;</code>。</p>
<div>
<img src="../../../../resource/2025/nginx/neo4j_access_ng.jpg" width="650" />
</div>
<p>若想将文件目录设置成资源服务器,在配置中打开<code class="language-plaintext highlighter-rouge">autoindex on;</code>和<code class="language-plaintext highlighter-rouge">index nothing;</code>,需注释<code class="language-plaintext highlighter-rouge">index index.html index.htm;</code>,因为即使配置了目录检索,当目录下存在<code class="language-plaintext highlighter-rouge">index.html</code>时,默认也是打开<code class="language-plaintext highlighter-rouge">index.html</code>。</p>
<div>
<img src="../../../../resource/2025/nginx/neo4j_dir_ng.jpg" width="650" />
</div>
<p>提升浏览器获取静态资源的速度,在<code class="language-plaintext highlighter-rouge">nginx.conf</code>中打开<code class="language-plaintext highlighter-rouge">gzip on;</code>,同时也可指定<code class="language-plaintext highlighter-rouge">gzip_min_length</code>、<code class="language-plaintext highlighter-rouge">gzip_comp_level</code>、<code class="language-plaintext highlighter-rouge">gzip_types</code>内容,对静态资源使用<code class="language-plaintext highlighter-rouge">gzip</code>进行压缩。</p>
<h3 id="goaccess实时监控nginx访问"><code class="language-plaintext highlighter-rouge">GoAccess</code>实时监控<code class="language-plaintext highlighter-rouge">nginx</code>访问</h3>
<p>在<code class="language-plaintext highlighter-rouge">nginx</code>的<code class="language-plaintext highlighter-rouge">logs</code>目录中有请求访问的日志,在<code class="language-plaintext highlighter-rouge">location</code>中的配置,首先<code class="language-plaintext highlighter-rouge">log_format</code>定义了访问日志的格式,在<code class="language-plaintext highlighter-rouge">server</code>中定义了<code class="language-plaintext highlighter-rouge">access_log</code>的路径以及应用的格式。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># nginx log日志的格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
server {
# ...
access_log logs/neo4jdoc.access.log main;
}
</code></pre></div></div>
<p>从<code class="language-plaintext highlighter-rouge">GoAccess</code>网站下载安装包,很奇怪,<code class="language-plaintext highlighter-rouge">C</code>相关的应用下载都是源码包,没有针对于特定系统的二进制包,还需安装依赖包<code class="language-plaintext highlighter-rouge">libmaxminddb</code>,否则安装时会报错<code class="language-plaintext highlighter-rouge">** Missing development files for libmaxminddb library</code>。</p>
<p>从<code class="language-plaintext highlighter-rouge">https://github.com/maxmind/libmaxminddb</code> 下载安装包,在本地环境解压,进入到<code class="language-plaintext highlighter-rouge">libmaxminddb</code>目录,执行<code class="language-plaintext highlighter-rouge">./configure</code>然后执行<code class="language-plaintext highlighter-rouge">make install</code>进行安装。然后进入<code class="language-plaintext highlighter-rouge">GoAccess</code>安装包的解压目录,执行:<code class="language-plaintext highlighter-rouge">./configure --enable-utf8 --enable-geoip=mmdb</code>、<code class="language-plaintext highlighter-rouge">make install</code>指令进行安装。</p>
<p>进入<code class="language-plaintext highlighter-rouge">nginx</code>的<code class="language-plaintext highlighter-rouge">logs</code>目录,执行实时监控命令,生成<code class="language-plaintext highlighter-rouge">report.html</code>,同时在<code class="language-plaintext highlighter-rouge">nginx.conf</code>配置<code class="language-plaintext highlighter-rouge">report.html</code>的路由,以便在浏览器中访问监控页面。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LANG="en_US.UTF-8" LC_TIME="en_US.UTF-8" bash -c 'goaccess neo4jdoc.access.log -o ../html/report.html --real-time-html --log-format=COMBINED'
server {
#... 对外暴露report.html页面
location /report.html {
alias /Users/madong/software/c_nginx_1.28.0/html/report.html;
}
}
</code></pre></div></div>
<p>在浏览器中输入<code class="language-plaintext highlighter-rouge">http://127.0.0.1:8093/report.html</code> 即可以看到监控页面,有一点,在<code class="language-plaintext highlighter-rouge">html</code>页面上一开始可能会展示<code class="language-plaintext highlighter-rouge">unauthorized</code>,等过一会儿<code class="language-plaintext highlighter-rouge">web socket</code>就能建立成功,就可以展示页面上的指标,例如:请求命中数、请求文件<code class="language-plaintext highlighter-rouge">url</code>统计、静态请求数、<code class="language-plaintext highlighter-rouge">404</code>的<code class="language-plaintext highlighter-rouge">url</code>统计等。</p>
<div>
<img src="../../../../resource/2025/nginx/goaccess_report_ng.jpg" width="650" />
<img src="../../../../resource/2025/nginx/goaccess_ng_panel2.jpg" width="650" />
</div>
<h3 id="nginx常用指令"><code class="language-plaintext highlighter-rouge">nginx</code>常用指令</h3>
<ul>
<li><code class="language-plaintext highlighter-rouge">listen</code>指令,一个请求进入<code class="language-plaintext highlighter-rouge">nginx</code>之前,首先要监听端口,会用到<code class="language-plaintext highlighter-rouge">listen</code>指令,指令的语法有:<code class="language-plaintext highlighter-rouge">listen address[:port]、listen port</code>。
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>listen *:8000; # 监听机器上的8000端口,访问ip不限制
listen 127.0.0.1:8000; # 限制ip为127。0.0.1
listen unix:/var/run/nginx.sock; # 监听unix的websocket
</code></pre></div> </div>
</li>
<li><code class="language-plaintext highlighter-rouge">server_name</code>指令,此指令后面跟多个域名时,第一个域名为主域名,有个默认配置<code class="language-plaintext highlighter-rouge">server_name_in_redirect off</code>(默认关闭)。在打开配置时,当需要重定向请求,但未指定域名时,则会使用主域名。
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server_name taohui.tech tech.nginx; # 第一个为主域名
server_name *.taohui.tech; # 泛域名,仅支持在最前或最后
</code></pre></div> </div>
</li>
<li><code class="language-plaintext highlighter-rouge">return</code>和<code class="language-plaintext highlighter-rouge">error_page</code>指令,语法格式为:<code class="language-plaintext highlighter-rouge">return code [text|URL]</code>,<code class="language-plaintext highlighter-rouge">error_page</code>指令是对错误页面的重定向,例如<code class="language-plaintext highlighter-rouge">403</code>转发到未授权页面。
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>return 301 url; # 301表示永久重定向,302临时重定向,禁止被缓存
error_page 404 /404.html;
error_page 403 http://example.com/forbidden.html;
</code></pre></div> </div>
</li>
<li><code class="language-plaintext highlighter-rouge">location</code>指令,此指令常用于寻找配置,语法为:<code class="language-plaintext highlighter-rouge">location url|@name</code>支持<code class="language-plaintext highlighter-rouge">=</code>精确匹配和正则表达式(<code class="language-plaintext highlighter-rouge">^</code>)匹配路径,当没有等号时,则应用前缀匹配原则(最长匹配)。
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>location <span class="o">=</span> /Test1 <span class="o">{</span> <span class="c"># 精确匹配</span>
<span class="k">return </span>200 <span class="s1">'exact match'</span><span class="o">!</span><span class="p">;</span>
<span class="o">}</span>
location /Test1 <span class="o">{</span> <span class="c"># 前缀匹配,同时满足时,优先精确匹配</span>
<span class="k">return </span>200 <span class="s1">'prefix string match!'</span><span class="p">;</span>
<span class="o">}</span>
location ^~/Test1/ <span class="o">{</span> <span class="c"># 正则表达式匹配</span>
<span class="k">return </span>200 <span class="s1">'regular expression match!'</span><span class="p">;</span>
<span class="o">}</span>
</code></pre></div> </div>
</li>
<li><code class="language-plaintext highlighter-rouge">access</code>阶段指令,其中包含<code class="language-plaintext highlighter-rouge">allow</code>和<code class="language-plaintext highlighter-rouge">deny</code>,上下文作用于<code class="language-plaintext highlighter-rouge">http</code>、<code class="language-plaintext highlighter-rouge">server</code>、<code class="language-plaintext highlighter-rouge">context</code>,表示允许或禁止某些指令访问。
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>deny 192.168.1.1;
allow 192.168.1.0/24;
</code></pre></div> </div>
</li>
<li><code class="language-plaintext highlighter-rouge">content</code>阶段的指令,<code class="language-plaintext highlighter-rouge">root</code>和<code class="language-plaintext highlighter-rouge">alias</code>指令,它们之间的区别:<code class="language-plaintext highlighter-rouge">root</code>会将完整url映射进文件路径中(<code class="language-plaintext highlighter-rouge">root</code>路径+<code class="language-plaintext highlighter-rouge">location</code>路径),<code class="language-plaintext highlighter-rouge">alias</code>之后将<code class="language-plaintext highlighter-rouge">location</code>后的<code class="language-plaintext highlighter-rouge">url</code>路径映射到文件路径。
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>location /root <span class="o">{</span> <span class="c"># curl请求http://127.0.0.1:8093/root时,对应静态资源路径为:html/root/index.html</span>
root html<span class="p">;</span>
<span class="o">}</span>
location /alias <span class="o">{</span> <span class="c"># curl请求http://127.0.0.1:8093/alias时,对应静态资源路径为:html/index.html</span>
<span class="nb">alias </span>html<span class="p">;</span>
<span class="o">}</span>
</code></pre></div> </div>
</li>
</ul>
Go内存分配与GC
2025-03-28T00:00:00+00:00
https://dongma.github.io/2025/03/28/go内存分配与GC
<h3 id="go语言gmp介绍">Go语言GMP介绍</h3>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">Go</code>语言相比<code class="language-plaintext highlighter-rouge">Java</code>,有更好的并发能力(<code class="language-plaintext highlighter-rouge">GMP</code>模型),同时其占用的服务器资源也较少,了解一下<code class="language-plaintext highlighter-rouge">GMP</code>的理念。从操作系统层面来看,线程是指内核级线程,是操作系统最小调度单元,创建、销毁、调度交由内核完成,可充分利用多核。协程(用户线程)与线程存在<code class="language-plaintext highlighter-rouge">M:1</code>的映射关系,从属于同一个内存级线程,无法并行,并且,一个协程阻塞会导致从属同一线程的所有协程无法执行。</p>
</blockquote>
<h3 id="goroutine">Goroutine</h3>
<p>经<code class="language-plaintext highlighter-rouge">Golang</code>优化后的协程,其有如下特点:1)与线程存在映射关系,为<code class="language-plaintext highlighter-rouge">M:N</code>;2)创建、销毁、调度在用户态完成,对内核透明,足够轻便;3)可利用多个线程,实现并行;4)通过调度器的斡旋,实现和线程间的动态绑定和灵活调度;5)栈空间大小可动态扩缩,因地制宜;</p>
<p>在<code class="language-plaintext highlighter-rouge">/runtime/proc.go</code>的代码注释中,有对<code class="language-plaintext highlighter-rouge">GMP</code>的解释,其核心数据结构在<code class="language-plaintext highlighter-rouge">/runtime/runtime2.go</code>:
<!-- more --></p>
<ul>
<li>其中<code class="language-plaintext highlighter-rouge">g</code>是<code class="language-plaintext highlighter-rouge">Golang</code>中对协程的抽象,<code class="language-plaintext highlighter-rouge">g</code>有自己的运行栈,状态及执行的任务函数,<code class="language-plaintext highlighter-rouge">g</code>需要绑定到<code class="language-plaintext highlighter-rouge">p</code>上才能执行,<code class="language-plaintext highlighter-rouge">p</code>就是<code class="language-plaintext highlighter-rouge">g</code>的<code class="language-plaintext highlighter-rouge">cpu</code>;</li>
<li><code class="language-plaintext highlighter-rouge">m</code>即<code class="language-plaintext highlighter-rouge">machine</code>,是<code class="language-plaintext highlighter-rouge">golang</code>中对线程的抽象,<code class="language-plaintext highlighter-rouge">m</code>不直接执行<code class="language-plaintext highlighter-rouge">g</code>,而是先和<code class="language-plaintext highlighter-rouge">p</code>绑定,由其代理执行;借由<code class="language-plaintext highlighter-rouge">p</code>的存在,<code class="language-plaintext highlighter-rouge">m</code>无需和<code class="language-plaintext highlighter-rouge">g</code>绑死,也无需记录<code class="language-plaintext highlighter-rouge">g</code>的状态信息,因此<code class="language-plaintext highlighter-rouge">g</code>在全生命周期中可以跨<code class="language-plaintext highlighter-rouge">m</code>执行。</li>
<li><code class="language-plaintext highlighter-rouge">p</code>也即<code class="language-plaintext highlighter-rouge">processor</code>,是<code class="language-plaintext highlighter-rouge">golang</code>中的调度器。对于<code class="language-plaintext highlighter-rouge">g</code>而言,<code class="language-plaintext highlighter-rouge">p</code>是其调度器,<code class="language-plaintext highlighter-rouge">g</code>只有被<code class="language-plaintext highlighter-rouge">p</code>调度,才得以执行;对<code class="language-plaintext highlighter-rouge">m</code>而言,<code class="language-plaintext highlighter-rouge">p</code>是其执行代理,为其提供必要信息的同时,隐藏了繁杂的调度细节;</li>
</ul>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Goroutine scheduler, Design doc at https://golang.org/s/go11sched.</span>
<span class="c">// The scheduler's job is to distribute ready-to-run goroutines over worker threads.</span>
<span class="c">//</span>
<span class="c">// The main concepts are:</span>
<span class="c">// G - goroutine.</span>
<span class="c">// M - worker thread, or machine.</span>
<span class="c">// P - processor, a resource that is required to execute Go code.</span>
<span class="c">// M must have an associated P to execute Go code, however it can be</span>
<span class="c">// blocked or in a syscall w/o an associated P.</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">gmp</code>模型其要点和调度规则如下:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">M</code>是线程的抽象;<code class="language-plaintext highlighter-rouge">G</code>是<code class="language-plaintext highlighter-rouge">goroutine</code>;<code class="language-plaintext highlighter-rouge">P</code>是承上启下的调度器;<code class="language-plaintext highlighter-rouge">M</code>调度<code class="language-plaintext highlighter-rouge">G</code>前,需要和<code class="language-plaintext highlighter-rouge">P</code>绑定;</li>
<li>全局有多个<code class="language-plaintext highlighter-rouge">M</code>和多个<code class="language-plaintext highlighter-rouge">P</code>,但同时并行的<code class="language-plaintext highlighter-rouge">G</code>的最大数量等于<code class="language-plaintext highlighter-rouge">P</code>的数量;</li>
<li><code class="language-plaintext highlighter-rouge">G</code>的存放队列有三类:<code class="language-plaintext highlighter-rouge">P</code>的本地队列、全局队列和<code class="language-plaintext highlighter-rouge">wait</code>队列(图中未展示,为io阻塞就绪态<code class="language-plaintext highlighter-rouge">goroutine</code>队列);</li>
<li><code class="language-plaintext highlighter-rouge">M</code>调度<code class="language-plaintext highlighter-rouge">G</code>时,优先取<code class="language-plaintext highlighter-rouge">P</code>本地队列,其次取全局队列,最后取<code class="language-plaintext highlighter-rouge">wait</code>队列;这样的好处是,取本地队列时,可以接近于无锁化,减少全局锁竞争;</li>
<li>为防止不同P的闲忙差异过大,设立<code class="language-plaintext highlighter-rouge">work-stealing</code>机制,本地队列为空的<code class="language-plaintext highlighter-rouge">P</code>可以尝试从其他<code class="language-plaintext highlighter-rouge">P</code>本地队列偷取一半的<code class="language-plaintext highlighter-rouge">G</code>补充到自身队列;</li>
</ul>
<h3 id="gmp调度">GMP调度</h3>
<p><code class="language-plaintext highlighter-rouge">g0</code>是一种特殊的调度协程,不执行用户函数,负责执行<code class="language-plaintext highlighter-rouge">g</code>之间的切换调度,与<code class="language-plaintext highlighter-rouge">m</code>的关系为<code class="language-plaintext highlighter-rouge">1:1</code>。<code class="language-plaintext highlighter-rouge">goroutine</code>的类型可分为两类:</p>
<ul>
<li>1)负责调度普通<code class="language-plaintext highlighter-rouge">g</code>的<code class="language-plaintext highlighter-rouge">g0</code>,与<code class="language-plaintext highlighter-rouge">m</code>的关系为一对一;</li>
<li>2)负责执行用户函数的普通<code class="language-plaintext highlighter-rouge">g</code>,被调度执行的<code class="language-plaintext highlighter-rouge">g</code>永远在<code class="language-plaintext highlighter-rouge">g</code>和<code class="language-plaintext highlighter-rouge">g0</code>的状态间切换;</li>
</ul>
<p>当<code class="language-plaintext highlighter-rouge">g0</code>找到可执行<code class="language-plaintext highlighter-rouge">g</code>时,会调用<code class="language-plaintext highlighter-rouge">gogo</code>方法,调度<code class="language-plaintext highlighter-rouge">g</code>执行用户定义的任务。当<code class="language-plaintext highlighter-rouge">g</code>需要主动让渡时,会触发<code class="language-plaintext highlighter-rouge">mcall</code>方法,将执行权限重新交给<code class="language-plaintext highlighter-rouge">g0</code>;</p>
<p>广义”调度”可分为几种类型:</p>
<ul>
<li>主动调度:一种用户主动执行让渡过程,主要方式是在代码中执行<code class="language-plaintext highlighter-rouge">runtime.Gosched</code>方法(<code class="language-plaintext highlighter-rouge">runtime/proc.go</code>),此时当前<code class="language-plaintext highlighter-rouge">g</code>会当让出执行权,主动进行队列等待下次被调度执行。</li>
<li>被动调度:因不满足某执行条件,<code class="language-plaintext highlighter-rouge">g</code>可能陷入阻塞态无法被调度,直到关注的条件达成后,<code class="language-plaintext highlighter-rouge">g</code>才从阻塞中被唤醒(对应<code class="language-plaintext highlighter-rouge">runtime/proc.go#gopark</code>方法,恢复则是<code class="language-plaintext highlighter-rouge">goready</code>方法)。</li>
<li>正常调度: <code class="language-plaintext highlighter-rouge">g</code>中的执行任务已完成,<code class="language-plaintext highlighter-rouge">g0</code>会将当前<code class="language-plaintext highlighter-rouge">g</code>置为死亡状态,发起新一轮调度;</li>
<li>抢占调度:倘若<code class="language-plaintext highlighter-rouge">g</code>执行系统调用超过指定的时长,且全局<code class="language-plaintext highlighter-rouge">p</code>资源比较短缺,此时将<code class="language-plaintext highlighter-rouge">p</code>和<code class="language-plaintext highlighter-rouge">g</code>接绑,用解绑的<code class="language-plaintext highlighter-rouge">p</code>用于其他<code class="language-plaintext highlighter-rouge">g</code>的调度;</li>
</ul>
<p>值得一提的是,前<code class="language-plaintext highlighter-rouge">3</code>种调度方式都由<code class="language-plaintext highlighter-rouge">m</code>下的<code class="language-plaintext highlighter-rouge">g0</code>完成。而抢占调用则是由一个全局监控协程<code class="language-plaintext highlighter-rouge">monitor g</code>来监控,倘若发现满足抢占调度的条件,则会从第三方的角度出手干预,主动发起该动作。从宏观上:</p>
<ul>
<li>以<code class="language-plaintext highlighter-rouge">g0</code> -> <code class="language-plaintext highlighter-rouge">g</code> -> <code class="language-plaintext highlighter-rouge">g0</code> 的一轮循环为例进行串联;</li>
<li><code class="language-plaintext highlighter-rouge">g0</code> 执行 <code class="language-plaintext highlighter-rouge">schedule()</code> 函数,寻找到用于执行的 <code class="language-plaintext highlighter-rouge">g</code>;</li>
<li><code class="language-plaintext highlighter-rouge">g0</code> 执行 <code class="language-plaintext highlighter-rouge">execute()</code> 方法,更新当前 <code class="language-plaintext highlighter-rouge">g</code>、<code class="language-plaintext highlighter-rouge">p</code> 的状态信息,并调用<code class="language-plaintext highlighter-rouge">gogo()</code>方法,将执行权交给<code class="language-plaintext highlighter-rouge">g</code>;</li>
<li><code class="language-plaintext highlighter-rouge">g</code> 因主动让渡(<code class="language-plaintext highlighter-rouge">gosche_m()</code>)、被动调度(<code class="language-plaintext highlighter-rouge">park_m()</code>)、正常结束(<code class="language-plaintext highlighter-rouge">goexit0()</code>)等原因,调用<code class="language-plaintext highlighter-rouge">m_call</code>函数,执行权重新回到<code class="language-plaintext highlighter-rouge">g0</code>手中;</li>
<li><code class="language-plaintext highlighter-rouge">g0</code>执行<code class="language-plaintext highlighter-rouge">schedule()</code>函数,开启新一轮循环.</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">p</code>每执行<code class="language-plaintext highlighter-rouge">61</code>次,会从全局队列中获取一个<code class="language-plaintext highlighter-rouge">goroutine</code>进行执行,同时会额外将全局队列中的一个<code class="language-plaintext highlighter-rouge">goroutine</code>放到本地队列中。若本地队列已满,则会返回来将本地队列中一半的<code class="language-plaintext highlighter-rouge">g</code>放回全局队列中,帮助当前<code class="language-plaintext highlighter-rouge">p</code>缓解执行压力;</p>
<h2 id="go内存模型与分配机制">Go内存模型与分配机制</h2>
<blockquote>
<p>在操作系统中,存在<code class="language-plaintext highlighter-rouge">寄存器</code>、<code class="language-plaintext highlighter-rouge">高速缓存</code>、<code class="language-plaintext highlighter-rouge">内存</code>和<code class="language-plaintext highlighter-rouge">磁盘</code>,越接近<code class="language-plaintext highlighter-rouge">cpu</code>存储的容量越小,其对应的价格就越高昂。页表、分页管理等机制来减少内存碎片。</p>
</blockquote>
<p><code class="language-plaintext highlighter-rouge">Golang</code>中的内存模型,以空间换时间,一次缓存,多次复用。堆<code class="language-plaintext highlighter-rouge">mheap</code>正是基于该思想,产生的数据结构。依次细化粒度,建立了 mcentral、mcache 的模型,下面对三者作个梳理:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">mheap</code>:全局的内存起源,访问要加全局锁;</li>
<li><code class="language-plaintext highlighter-rouge">mcentral</code>:每种对象大小规格(全局共划分为<code class="language-plaintext highlighter-rouge">68</code>种)对应的缓存,锁的粒度也仅限于同一种规格以内;</li>
<li><code class="language-plaintext highlighter-rouge">mcache</code>:每个<code class="language-plaintext highlighter-rouge">P</code>(正是<code class="language-plaintext highlighter-rouge">GMP</code>中的<code class="language-plaintext highlighter-rouge">P</code>)持有一份的内存缓存,访问时无锁,多级规格,提高利用率;</li>
</ul>
<div>
<img src="https://pic4.zhimg.com/v2-f1126996923c66a21902883b60eb278b_r.jpg" width="660" />
</div>
<h3 id="内存单元mspan">内存单元<code class="language-plaintext highlighter-rouge">mspan</code></h3>
<p><code class="language-plaintext highlighter-rouge">page</code>和<code class="language-plaintext highlighter-rouge">mspan</code>的<code class="language-plaintext highlighter-rouge">2</code>个概念,page:最小的存储单元,默认大小为<code class="language-plaintext highlighter-rouge">8KB</code>,<code class="language-plaintext highlighter-rouge">mspan</code>大小为<code class="language-plaintext highlighter-rouge">page</code>的整数倍,且从<code class="language-plaintext highlighter-rouge">8B</code>到<code class="language-plaintext highlighter-rouge">80</code>KB 被划分为<code class="language-plaintext highlighter-rouge">67</code>种不同的规格,对应源代码在<code class="language-plaintext highlighter-rouge">runtime/sizeclasses.go</code>,<code class="language-plaintext highlighter-rouge">mspan</code>具有如下特点:</p>
<ul>
<li>根据规格大小,产生了等级的制度,<code class="language-plaintext highlighter-rouge">mspan</code>是<code class="language-plaintext highlighter-rouge">Golang</code>内存管理的最小单元,<code class="language-plaintext highlighter-rouge">runtime/mheap.go</code>;</li>
<li>消除了外部碎片,但不可避免会有内部碎片;</li>
<li>宏观上能提高整体空间利用率,同等级的<code class="language-plaintext highlighter-rouge">mspan</code>会从属同一个<code class="language-plaintext highlighter-rouge">mcentral</code>,最终会被组织成链表,因此带有前后指针(<code class="language-plaintext highlighter-rouge">prev</code>、<code class="language-plaintext highlighter-rouge">next</code>);</li>
<li>正是因为有了规格等级的概念,才支持<code class="language-plaintext highlighter-rouge">mcentral</code>实现细锁化,全局总览,留个印象;</li>
<li><code class="language-plaintext highlighter-rouge">mspan</code>会基于<code class="language-plaintext highlighter-rouge">bitMap</code>辅助快速找到空闲内存块(块大小为对应等级下的<code class="language-plaintext highlighter-rouge">object</code>大小),此时需要使用到<code class="language-plaintext highlighter-rouge">Ctz64</code>算法.</li>
</ul>
<div>
<img src="https://pic4.zhimg.com/v2-1a053695aa6f80692753800c92b5c76f_r.jpg" width="660" />
</div>
<h3 id="线程缓存mcache">线程缓存<code class="language-plaintext highlighter-rouge">mcache</code></h3>
<ul>
<li><code class="language-plaintext highlighter-rouge">mcache</code>是每个<code class="language-plaintext highlighter-rouge">P</code>独有的缓存,因此交互无锁;</li>
<li><code class="language-plaintext highlighter-rouge">mcache</code>将每种<code class="language-plaintext highlighter-rouge">spanClass</code>等级的<code class="language-plaintext highlighter-rouge">mspan</code>各缓存了一个,总数为<code class="language-plaintext highlighter-rouge">2</code>(<code class="language-plaintext highlighter-rouge">nocan</code>维度) * <code class="language-plaintext highlighter-rouge">68</code>(大小维度)= <code class="language-plaintext highlighter-rouge">136</code>;</li>
<li><code class="language-plaintext highlighter-rouge">mcache</code>中还有一个为对象分配器<code class="language-plaintext highlighter-rouge">tiny allocator</code>,用于处理小于<code class="language-plaintext highlighter-rouge">16B</code>对象的内存分配;</li>
</ul>
<div>
<img src="https://pic4.zhimg.com/v2-2ec4168fd9670e48c4c322c004cb5b3f_1440w.jpg" width="660" />
</div>
<h3 id="中心缓存mcentral">中心缓存<code class="language-plaintext highlighter-rouge">mcentral</code></h3>
<p>要点:</p>
<ul>
<li>每个<code class="language-plaintext highlighter-rouge">mcentral</code>对应一种<code class="language-plaintext highlighter-rouge">spanClass</code>;</li>
<li>每个<code class="language-plaintext highlighter-rouge">mcentral</code>下聚合了该<code class="language-plaintext highlighter-rouge">spanClass</code>下的<code class="language-plaintext highlighter-rouge">mspan</code>;</li>
<li><code class="language-plaintext highlighter-rouge">mcentral</code>下的<code class="language-plaintext highlighter-rouge">mspan</code>分为两个链表,分别为有空间<code class="language-plaintext highlighter-rouge">mspan</code>链表<code class="language-plaintext highlighter-rouge">partial</code>和满空间<code class="language-plaintext highlighter-rouge">mspan</code>链表<code class="language-plaintext highlighter-rouge">full</code>`;</li>
<li>每个<code class="language-plaintext highlighter-rouge">mcentral</code>一把锁;</li>
</ul>
<div>
<img src="https://pic2.zhimg.com/v2-f2af84fcc77d9fbe4506e93e25b7343b_1440w.jpg" width="465" />
</div>
<h3 id="全局堆缓存mheap">全局堆缓存<code class="language-plaintext highlighter-rouge">mheap</code></h3>
<ul>
<li>对于<code class="language-plaintext highlighter-rouge">Golang</code>上层应用而言,堆是操作系统虚拟内存的抽象,以页(8KB)为单位,作为最小内存存储单元;</li>
<li>负责将连续页组装成<code class="language-plaintext highlighter-rouge">mspan</code>,全局内存基于<code class="language-plaintext highlighter-rouge">bitMap</code>标识其使用情况,每个<code class="language-plaintext highlighter-rouge">bit</code>对应一页,为<code class="language-plaintext highlighter-rouge">0</code>则自由,为<code class="language-plaintext highlighter-rouge">1</code>则已被<code class="language-plaintext highlighter-rouge">mspan</code>组装;</li>
<li>通过<code class="language-plaintext highlighter-rouge">heapArena</code>聚合页,记录了页到<code class="language-plaintext highlighter-rouge">mspan</code>的映射信息,建立空闲页基数树索引<code class="language-plaintext highlighter-rouge">radix tree index</code>,辅助快速寻找空闲页;</li>
<li>是<code class="language-plaintext highlighter-rouge">mcentral</code>的持有者,持有所有<code class="language-plaintext highlighter-rouge">spanClass</code>下的<code class="language-plaintext highlighter-rouge">mcentral</code>,作为自身的缓存,内存不够时,向操作系统申请,申请单位为 heapArena(64M);</li>
</ul>
<h3 id="对象分配流程">对象分配流程</h3>
<p>不论是以下哪种方式,最终都会殊途同归步入<code class="language-plaintext highlighter-rouge">mallocgc</code>方法中,例如:<code class="language-plaintext highlighter-rouge">new(T)</code>、<code class="language-plaintext highlighter-rouge">&T{}</code>、<code class="language-plaintext highlighter-rouge">make(xxxx)</code>,<code class="language-plaintext highlighter-rouge">Golang</code>中,依据<code class="language-plaintext highlighter-rouge">object</code>的大小,会将其分为下述三类:<code class="language-plaintext highlighter-rouge">tiny</code>微对象(0, 16B)、<code class="language-plaintext highlighter-rouge">small</code>小对象(16B,32KB)、<code class="language-plaintext highlighter-rouge">large</code>大对象(32KB,正无穷).
对于微对象的分配流程:</p>
<ol>
<li>从<code class="language-plaintext highlighter-rouge">P</code>专属<code class="language-plaintext highlighter-rouge">mcache</code>的<code class="language-plaintext highlighter-rouge">tiny</code>分配器取内存(无锁)</li>
<li>根据所属的<code class="language-plaintext highlighter-rouge">spanClass</code>,从<code class="language-plaintext highlighter-rouge">P</code>专属<code class="language-plaintext highlighter-rouge">mcache</code>缓存的<code class="language-plaintext highlighter-rouge">mspan</code>中取内存(无锁)</li>
<li>根据所属的<code class="language-plaintext highlighter-rouge">spanClass</code>从对应的<code class="language-plaintext highlighter-rouge">mcentral</code>中取<code class="language-plaintext highlighter-rouge">mspan</code>填充到<code class="language-plaintext highlighter-rouge">mcache</code>,然后从<code class="language-plaintext highlighter-rouge">mspan</code>中取内存(<code class="language-plaintext highlighter-rouge">spanClass</code>粒度锁);</li>
<li>根据所属的<code class="language-plaintext highlighter-rouge">spanClass</code>,从<code class="language-plaintext highlighter-rouge">mheap</code>的页分配器<code class="language-plaintext highlighter-rouge">pageAlloc</code>取得足够数量空闲页组装成<code class="language-plaintext highlighter-rouge">mspan</code>填充到<code class="language-plaintext highlighter-rouge">mcache</code>,然后从<code class="language-plaintext highlighter-rouge">mspan</code>中取内存(全局锁);</li>
<li><code class="language-plaintext highlighter-rouge">mheap</code>`向操作系统申请内存,更新页分配器的索引信息,然后重复(4);</li>
</ol>
<p>对于小对象的分配流程是跳过(1)步,执行上述流程的(2)-(5)步; 对于大对象的分配流程是跳过(1)-(3)步,执行上述流程的(4)-(5)步.</p>
<h2 id="go垃圾回收原理">Go垃圾回收原理</h2>
<blockquote>
<p>做<code class="language-plaintext highlighter-rouge">Java</code>的都对<code class="language-plaintext highlighter-rouge">GC</code>比较熟悉,在<code class="language-plaintext highlighter-rouge">JVM</code>中常见的<code class="language-plaintext highlighter-rouge">GC</code>算法有:标记整理(Mark-Sweep)、标记压缩(Mark-Compact)、半空间复制(类似于<code class="language-plaintext highlighter-rouge">G1</code>),通过引用计数寻找不可达对象,便于垃圾回收。</p>
</blockquote>
<h3 id="go中三色标记法"><code class="language-plaintext highlighter-rouge">Go</code>中三色标记法</h3>
<p><code class="language-plaintext highlighter-rouge">Golang GC</code>中用到的三色标记法属于标记清扫-算法下的一种实现,由荷兰的计算机科学家<code class="language-plaintext highlighter-rouge">Dijkstra</code>提出,下面阐述要点:</p>
<ul>
<li>对象分为三种颜色标记:黑、灰、白,黑对象代表,对象自身存活,且其指向对象都已标记完成;</li>
<li>灰对象代表,对象自身存活,但其指向对象还未标记完成;白对象代表,对象尙未被标记到,可能是垃圾对象</li>
<li>标记开始前,将根对象(全局对象、栈上局部变量等)置黑,将其所指向的对象置灰;</li>
<li>标记规则是,从灰对象出发,将其所指向的对象都置灰. 所有指向对象都置灰后,当前灰对象置黑;</li>
<li>标记结束后,白色对象就是不可达的垃圾对象,需要进行清扫;</li>
</ul>
<div>
<img src="https://pic3.zhimg.com/v2-e5cfe71a2eb567f8681c8b9c6c3f71c0_r.jpg" width="660" />
</div>
<p>为了应对并发情况下,对象标记出现漏标、多标的情况,可使用屏障机制。漏标问题的本质就是,一个已经扫描完成的黑对象指向了一个被灰\白对象删除引用的白色对象. 一套用于解决漏标问题的方法论称之为强弱三色不变式:</p>
<ul>
<li>强三色不变式:白色对象不能被黑色对象直接引用;</li>
<li>弱三色不变式:白色对象可以被黑色对象引用,但要从某个灰对象出发仍然可达该白对象(间接破坏了(1)、(2)的联动);</li>
</ul>
golang sql体系及orm实现
2025-03-22T00:00:00+00:00
https://dongma.github.io/2025/03/22/go sql体系及orm实现
<h3 id="golang-sql标准库研究">golang sql标准库研究</h3>
<h3 id="抽象接口定义">抽象接口定义</h3>
<p><code class="language-plaintext highlighter-rouge">database/sql/driver/driver.go</code>关于数据库驱动模块下各核心<code class="language-plaintext highlighter-rouge">interface</code>主要包括:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Connector</code>: 抽象的数据库连接器,需要具备创建数据库连接以及返回从属的数据库驱动的能力;</li>
<li><code class="language-plaintext highlighter-rouge">Driver</code>: 抽象的数据库驱动,具备创建数据库连接的能力;</li>
<li><code class="language-plaintext highlighter-rouge">Conn</code>: 抽象的数据库连接,具备预处理<code class="language-plaintext highlighter-rouge">sql</code>以及开启事务的能力;</li>
<li><code class="language-plaintext highlighter-rouge">Tx</code>: 抽象的事务,具备提交和回滚的能力;</li>
<li><code class="language-plaintext highlighter-rouge">Statement</code>: 抽象的请求预处理状态. 具备实际执行<code class="language-plaintext highlighter-rouge">sql</code>并返回执行结果的能力;</li>
<li><code class="language-plaintext highlighter-rouge">Result/Row</code>: 抽象的<code class="language-plaintext highlighter-rouge">sql</code>执行结果;</li>
</ul>
<!-- more -->
<div>
<img src="https://pic2.zhimg.com/v2-bd981e76df19ad674518a4d434764693_r.jpg" width="580" />
</div>
<p>在<code class="language-plaintext highlighter-rouge">database/sql/sql.go</code>中定义的几个核心实体类. 核心内容主要是对于数据库连接池的实现以及对第三方数据库驱动能力的再封装.</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">DB</code>: 对应为数据库的具象化实例,其中包含如下几个核心字段:<code class="language-plaintext highlighter-rouge">connector</code>(用于创建数据库连接的抽象连接器,由第三方数据库提供具体实现)、<code class="language-plaintext highlighter-rouge">mu</code>、<code class="language-plaintext highlighter-rouge">freeConn</code>、<code class="language-plaintext highlighter-rouge">connRequests</code>等。</li>
<li><code class="language-plaintext highlighter-rouge">driverConn</code>: 其核心属性是由第三方驱动实现的<code class="language-plaintext highlighter-rouge">driver.Conn</code>,在此之上添加了时间属性、回调函数、状态标识等辅助信息;</li>
<li><code class="language-plaintext highlighter-rouge">driverStmt</code>: 在抽象的<code class="language-plaintext highlighter-rouge">driver.Stmt</code>基础上,添加了互斥锁、关闭状态标识等信息;</li>
<li><code class="language-plaintext highlighter-rouge">Tx</code>: 在抽象的<code class="language-plaintext highlighter-rouge">driver.TX</code>基础上,额外添加了互斥锁、数据库连接、连接释放函数、上下文等辅助属性;</li>
</ul>
<h3 id="创建数据库">创建数据库</h3>
<p>沿着<code class="language-plaintext highlighter-rouge">sql.Open</code>方法向下追溯,查看一下创建数据库实例的流程细节:</p>
<ul>
<li>首先校验对应的 driver 是否已注册;</li>
<li>接下来调用<code class="language-plaintext highlighter-rouge">OpenDB</code>方法执行真正的<code class="language-plaintext highlighter-rouge">db</code>实例创建操作,方法中会创建一个<code class="language-plaintext highlighter-rouge">DB</code>,启动一个<code class="language-plaintext highlighter-rouge">connectionOpener</code>协程,连接池资源不足时,用于补充创建连接;</li>
<li>在<code class="language-plaintext highlighter-rouge">connectionOpener</code>方法中,通过<code class="language-plaintext highlighter-rouge">for + select</code>多路复用的形式,保持协程的运行;</li>
</ul>
<div>
<img src="https://pic3.zhimg.com/v2-f7a7b3b08d6ee9c63e0d413c4063737c_1440w.jpg" width="580" />
</div>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// 创建数据库</span>
<span class="k">func</span> <span class="n">Open</span><span class="p">(</span><span class="n">driverName</span><span class="p">,</span> <span class="n">dataSourceName</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">DB</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="c">// 首先根据驱动类型获取数据库驱动, 导入mysql驱动时,会自动在drivers中注册,_ "github.com/go-sql-driver/mysql"</span>
<span class="n">driversMu</span><span class="o">.</span><span class="n">RLock</span><span class="p">()</span>
<span class="n">driveri</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">drivers</span><span class="p">[</span><span class="n">driverName</span><span class="p">]</span>
<span class="n">driversMu</span><span class="o">.</span><span class="n">RUnlock</span><span class="p">()</span>
<span class="k">if</span> <span class="o">!</span><span class="n">ok</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"sql: unknown driver %q (forgotten import?)"</span><span class="p">,</span> <span class="n">driverName</span><span class="p">)</span>
<span class="p">}</span>
<span class="c">// 若驱动实现了对应的连接器 connector,则获取之并进行 db 实例创建</span>
<span class="k">if</span> <span class="n">driverCtx</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">driveri</span><span class="o">.</span><span class="p">(</span><span class="n">driver</span><span class="o">.</span><span class="n">DriverContext</span><span class="p">);</span> <span class="n">ok</span> <span class="p">{</span>
<span class="n">connector</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">driverCtx</span><span class="o">.</span><span class="n">OpenConnector</span><span class="p">(</span><span class="n">dataSourceName</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">err</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">OpenDB</span><span class="p">(</span><span class="n">connector</span><span class="p">),</span> <span class="no">nil</span>
<span class="p">}</span>
<span class="c">// 默认使用 dsn 数据库连接器,进行 db 创建</span>
<span class="k">return</span> <span class="n">OpenDB</span><span class="p">(</span><span class="n">dsnConnector</span><span class="p">{</span><span class="n">dsn</span><span class="o">:</span> <span class="n">dataSourceName</span><span class="p">,</span> <span class="n">driver</span><span class="o">:</span> <span class="n">driveri</span><span class="p">}),</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="执行请求">执行请求</h3>
<p>在执行一次<code class="language-plaintext highlighter-rouge">db.Query()</code>请求中,其中核心步骤包括:获取数据库连接(通过调用<code class="language-plaintext highlighter-rouge">conn</code>方法完成),执行<code class="language-plaintext highlighter-rouge">sql</code>(通过调用<code class="language-plaintext highlighter-rouge">queryDC</code>方法完成)、归还/释放连接(通过在<code class="language-plaintext highlighter-rouge">queryDC</code>方法中调用<code class="language-plaintext highlighter-rouge">releaseConn</code>方法完成);</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">maxBadConnRetries</span> <span class="o">=</span> <span class="m">2</span>
<span class="c">// 执行查询类 sql</span>
<span class="k">func</span> <span class="p">(</span><span class="n">db</span> <span class="o">*</span><span class="n">DB</span><span class="p">)</span> <span class="n">QueryContext</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">query</span> <span class="kt">string</span><span class="p">,</span> <span class="n">args</span> <span class="o">...</span><span class="n">any</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Rows</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="k">var</span> <span class="n">rows</span> <span class="o">*</span><span class="n">Rows</span>
<span class="k">var</span> <span class="n">err</span> <span class="kt">error</span>
<span class="k">var</span> <span class="n">isBadConn</span> <span class="kt">bool</span>
<span class="c">// 最多可以因为 BadConn 类型的错误重试两次</span>
<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o"><</span> <span class="n">maxBadConnRetries</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
<span class="c">// 执行 sql,此时采用的是 连接池有缓存连接优先复用 的策略</span>
<span class="n">rows</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">query</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">cachedOrNewConn</span><span class="p">)</span>
<span class="c">// 属于 badConn 类型的错误可以重试</span>
<span class="n">isBadConn</span> <span class="o">=</span> <span class="n">errors</span><span class="o">.</span><span class="n">Is</span><span class="p">(</span><span class="n">err</span><span class="p">,</span> <span class="n">driver</span><span class="o">.</span><span class="n">ErrBadConn</span><span class="p">)</span>
<span class="k">if</span> <span class="o">!</span><span class="n">isBadConn</span> <span class="p">{</span>
<span class="k">break</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c">// 重试了两轮 badConn 错误后,第三轮会采用</span>
<span class="k">if</span> <span class="n">isBadConn</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">query</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">alwaysNewConn</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">rows</span><span class="p">,</span> <span class="n">err</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">conn</code>方法获取数据库连接:</p>
<ul>
<li>倘若启用了连接池策略且连接池中有可用的连接,则会优先获取该连接进行返回;</li>
<li>倘若当前连接数已达上限,则会将当前协程挂起,建立对应的<code class="language-plaintext highlighter-rouge">channel</code>添加到<code class="language-plaintext highlighter-rouge">connRequests map</code>中,等待有连接释放时被唤醒;</li>
<li>倘若连接数未达上限,则会调用第三方驱动的<code class="language-plaintext highlighter-rouge">connector</code>完成新连接的创建;</li>
</ul>
<p>归还数据库连接,使用完数据库连接后,需要尝试将其放还连接池中,入口方法为<code class="language-plaintext highlighter-rouge">releaseConn</code>;</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">dc</span> <span class="o">*</span><span class="n">driverConn</span><span class="p">)</span> <span class="n">releaseConn</span><span class="p">(</span><span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">dc</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">putConn</span><span class="p">(</span><span class="n">dc</span><span class="p">,</span> <span class="n">err</span><span class="p">,</span> <span class="no">true</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<div>
<img src="https://picx.zhimg.com/v2-b442be6fbd1b6180ad93616128b86dad_1440w.jpg" width="580" />
</div>
<h3 id="清理任务">清理任务</h3>
<p>接下来是<code class="language-plaintext highlighter-rouge">cleaner</code>协程的运行流程,整体是通过<code class="language-plaintext highlighter-rouge">for + select</code>的方式常驻运行.
其中,<code class="language-plaintext highlighter-rouge">cleaner</code>创建了一个定时器<code class="language-plaintext highlighter-rouge">ticker</code>,定时时间间隔会在<code class="language-plaintext highlighter-rouge">maxIdleTime</code>、<code class="language-plaintext highlighter-rouge">maxLifeTime</code>中取较小值,并基于秒级向上取整.
每一轮<code class="language-plaintext highlighter-rouge">ticker</code>触发后,会执行:</p>
<ul>
<li>判断当前<code class="language-plaintext highlighter-rouge">db</code>是否已关闭或者存活连接数是否为零,是的话退出当前<code class="language-plaintext highlighter-rouge">cleaner</code>协程</li>
<li>调用<code class="language-plaintext highlighter-rouge">connectionCleanerRunLocked</code>对连接池中过期的连接进行清理</li>
</ul>
<div>
<img src="https://pic1.zhimg.com/v2-ef8adb23ef7930365211d6c4fcacab8a_1440w.jpg" width="580" />
</div>
<h3 id="mysql驱动实现"><code class="language-plaintext highlighter-rouge">mysql</code>驱动实现</h3>
<p><code class="language-plaintext highlighter-rouge">go-sql-driver/mysql</code>的核心功能是,遵循<code class="language-plaintext highlighter-rouge">database/sql</code>标准库中预留的接口协议,提供出对应于<code class="language-plaintext highlighter-rouge">mysql</code>的实现版本,将和<code class="language-plaintext highlighter-rouge">mysql</code>服务端的数据传输、通信协议,预处理模式、事务操作等内容封装实现在其中.</p>
<p>驱动加载,数据库驱动. <code class="language-plaintext highlighter-rouge">mysql driver</code>时,只需要匿名导入<code class="language-plaintext highlighter-rouge">go-sql-driver/mysql</code>的<code class="language-plaintext highlighter-rouge">lib</code>包,即可完成<code class="language-plaintext highlighter-rouge">driver</code>的注册操作。其原理是:会默认调用<code class="language-plaintext highlighter-rouge">mysql</code>包的<code class="language-plaintext highlighter-rouge">init</code>方法。</p>
<p><strong>驱动类</strong>定义位于<code class="language-plaintext highlighter-rouge">driver.go</code>,名称为<code class="language-plaintext highlighter-rouge">MySQLDriver</code>,对应实现<code class="language-plaintext highlighter-rouge">Open</code>方法用于创建数据库连接,核心步骤包括: 解析<code class="language-plaintext highlighter-rouge">dsn</code>,转为配置类实例、 构造连接器实例、 通过连接器完成连接创建操作;</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">(</span>
<span class="c">// 注册 mysql 数据库驱动</span>
<span class="n">_</span> <span class="s">"github.com/go-sql-driver/mysql"</span>
<span class="p">)</span>
<span class="c">// mysql#driver.go, This variable can be replaced with -ldflags like below:</span>
<span class="c">// go build "-ldflags=-X github.com/go-sql-driver/mysql.driverName=custom"</span>
<span class="k">var</span> <span class="n">driverName</span> <span class="o">=</span> <span class="s">"mysql"</span>
<span class="k">func</span> <span class="n">init</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">driverName</span> <span class="o">!=</span> <span class="s">""</span> <span class="p">{</span>
<span class="n">sql</span><span class="o">.</span><span class="n">Register</span><span class="p">(</span><span class="n">driverName</span><span class="p">,</span> <span class="o">&</span><span class="n">MySQLDriver</span><span class="p">{})</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c">// MySQL 版本的数据库驱动</span>
<span class="k">type</span> <span class="n">MySQLDriver</span> <span class="k">struct</span><span class="p">{}</span>
</code></pre></div></div>
<p><strong>连接器</strong>的实现位于<code class="language-plaintext highlighter-rouge">connecto.go</code>,其需实现<code class="language-plaintext highlighter-rouge">database/sql connector</code>接口定义的<code class="language-plaintext highlighter-rouge">Connect</code>和<code class="language-plaintext highlighter-rouge">Driver()</code>方法:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">connector</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">cfg</span> <span class="o">*</span><span class="n">Config</span> <span class="c">// immutable private copy.</span>
<span class="n">encodedAttributes</span> <span class="kt">string</span> <span class="c">// Encoded connection attributes.</span>
<span class="p">}</span>
<span class="c">// Connect implements driver.Connector interface.</span>
<span class="c">// Connect returns a connection to the database.</span>
<span class="k">func</span> <span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">connector</span><span class="p">)</span> <span class="n">Connect</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">(</span><span class="n">driver</span><span class="o">.</span><span class="n">Conn</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="c">// New mysqlConn</span>
<span class="n">mc</span> <span class="o">:=</span> <span class="o">&</span><span class="n">mysqlConn</span><span class="p">{</span>
<span class="n">maxAllowedPacket</span><span class="o">:</span> <span class="n">maxPacketSize</span><span class="p">,</span>
<span class="n">maxWriteSize</span><span class="o">:</span> <span class="n">maxPacketSize</span> <span class="o">-</span> <span class="m">1</span><span class="p">,</span>
<span class="n">closech</span><span class="o">:</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="k">struct</span><span class="p">{}),</span>
<span class="n">cfg</span><span class="o">:</span> <span class="n">cfg</span><span class="p">,</span>
<span class="n">connector</span><span class="o">:</span> <span class="n">c</span><span class="p">,</span>
<span class="p">}</span>
<span class="c">// ...</span>
<span class="p">}</span>
<span class="c">// Driver implements driver.Connector interface.</span>
<span class="c">// Driver returns &MySQLDriver{}.</span>
<span class="k">func</span> <span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">connector</span><span class="p">)</span> <span class="n">Driver</span><span class="p">()</span> <span class="n">driver</span><span class="o">.</span><span class="n">Driver</span> <span class="p">{</span>
<span class="k">return</span> <span class="o">&</span><span class="n">MySQLDriver</span><span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Connect</code>方法的实现主要包含如下几个核心步骤,与<code class="language-plaintext highlighter-rouge">mysql</code>连接配置有关的内容被聚合在<code class="language-plaintext highlighter-rouge">dsn.go</code>:</p>
<ul>
<li>创建连接(<code class="language-plaintext highlighter-rouge">net.Dialer.DialContext</code>)、设置为<code class="language-plaintext highlighter-rouge">tcp</code>长连接(<code class="language-plaintext highlighter-rouge">net.TCPConn.KeepAlive</code>)、创建连接缓冲区(<code class="language-plaintext highlighter-rouge">mc.buf = newBuffer</code>)</li>
<li>设置连接超时配置(<code class="language-plaintext highlighter-rouge">mc.buf.timeout = mc.cfg.ReadTimeout</code>;<code class="language-plaintext highlighter-rouge">mc.writeTimeout = mc.cfg.WriteTimeout</code>)</li>
<li>接收来自服务端的握手请求(<code class="language-plaintext highlighter-rouge">mc.readHandshakePacket</code>)、向服务端发起鉴权请求(<code class="language-plaintext highlighter-rouge">mc.writeHandshakeResponsePacket</code>)</li>
<li>处理鉴权结果(<code class="language-plaintext highlighter-rouge">mc.handleAuthResult</code>)、设置<code class="language-plaintext highlighter-rouge">dsn</code>中的参数变量(<code class="language-plaintext highlighter-rouge">mc.handleParams</code>)</li>
</ul>
<div>
<img src="https://pica.zhimg.com/v2-c12dd5ef35b5ce049f73d6d41483dd9e_r.jpg" width="580" />
</div>
<p><strong>数据库连接</strong>接口,值得一提的是,在使用<code class="language-plaintext highlighter-rouge">mysqlConn</code>的过程中,在文件<code class="language-plaintext highlighter-rouge">connection.go</code>中,<code class="language-plaintext highlighter-rouge">mysqlConn</code>对外可以通过公开方法<code class="language-plaintext highlighter-rouge">Close</code>实现关闭:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">mysqlConn</span> <span class="k">struct</span> <span class="p">{</span>
<span class="c">// 缓冲区数据</span>
<span class="n">buf</span> <span class="n">buffer</span>
<span class="c">// 网络连接</span>
<span class="n">netConn</span> <span class="n">net</span><span class="o">.</span><span class="n">Conn</span>
<span class="n">rawConn</span> <span class="n">net</span><span class="o">.</span><span class="n">Conn</span> <span class="c">// underlying connection when netConn is TLS connection.</span>
<span class="n">result</span> <span class="n">mysqlResult</span> <span class="c">// sql 执行结果</span>
<span class="c">// ...</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">mc</span> <span class="o">*</span><span class="n">mysqlConn</span><span class="p">)</span> <span class="n">Close</span><span class="p">()</span> <span class="p">(</span><span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="c">// Makes Close idempotent</span>
<span class="k">if</span> <span class="o">!</span><span class="n">mc</span><span class="o">.</span><span class="n">closed</span><span class="o">.</span><span class="n">Load</span><span class="p">()</span> <span class="p">{</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">mc</span><span class="o">.</span><span class="n">writeCommandPacket</span><span class="p">(</span><span class="n">comQuit</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">mc</span><span class="o">.</span><span class="n">cleanup</span><span class="p">()</span>
<span class="k">return</span>
<span class="p">}</span>
</code></pre></div></div>
<div>
<img src="https://pic3.zhimg.com/v2-73ba9c3bc715e2460aca3bd601d13aea_r.jpg" width="580" />
</div>
<p>下面是通过<code class="language-plaintext highlighter-rouge">mysqlConn</code>执行<strong>查询类请求</strong>的流程,对于<code class="language-plaintext highlighter-rouge">query</code>方法,入参中的<code class="language-plaintext highlighter-rouge">query</code>字段为<code class="language-plaintext highlighter-rouge">sql</code>模板,<code class="language-plaintext highlighter-rouge">args</code>字段为用于填充占位符的参数。</p>
<div>
<img src="https://pic2.zhimg.com/v2-683f9dd8063895c72054f5d39c65af13_r.jpg" width="580" />
</div>
<p><code class="language-plaintext highlighter-rouge">query</code>方法的出参类型为<code class="language-plaintext highlighter-rouge">textRows</code>,其首先会读取响应报文中第一部分,填充各个列的信息,后续内容会保留在内置的<code class="language-plaintext highlighter-rouge">conn</code>中,通过使用方调用<code class="language-plaintext highlighter-rouge">rows</code>的<code class="language-plaintext highlighter-rouge">Next</code>方法时再进行读取操作.</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">mc</span> <span class="o">*</span><span class="n">mysqlConn</span><span class="p">)</span> <span class="n">Query</span><span class="p">(</span><span class="n">query</span> <span class="kt">string</span><span class="p">,</span> <span class="n">args</span> <span class="p">[]</span><span class="n">driver</span><span class="o">.</span><span class="n">Value</span><span class="p">)</span> <span class="p">(</span><span class="n">driver</span><span class="o">.</span><span class="n">Rows</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">mc</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">args</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">mc</span> <span class="o">*</span><span class="n">mysqlConn</span><span class="p">)</span> <span class="n">query</span><span class="p">(</span><span class="n">query</span> <span class="kt">string</span><span class="p">,</span> <span class="n">args</span> <span class="p">[]</span><span class="n">driver</span><span class="o">.</span><span class="n">Value</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">textRows</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">handleOk</span> <span class="o">:=</span> <span class="n">mc</span><span class="o">.</span><span class="n">clearResult</span><span class="p">()</span>
<span class="c">// 连接已关闭?</span>
<span class="k">if</span> <span class="n">mc</span><span class="o">.</span><span class="n">closed</span><span class="o">.</span><span class="n">Load</span><span class="p">()</span> <span class="p">{</span>
<span class="n">mc</span><span class="o">.</span><span class="n">cfg</span><span class="o">.</span><span class="n">Logger</span><span class="o">.</span><span class="n">Print</span><span class="p">(</span><span class="n">ErrInvalidConn</span><span class="p">)</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">driver</span><span class="o">.</span><span class="n">ErrBadConn</span>
<span class="p">}</span>
<span class="c">// ...</span>
<span class="p">}</span>
</code></pre></div></div>
<p><strong>sql预处理</strong>,go-sql-driver/mysql 库实现的<code class="language-plaintext highlighter-rouge">statement</code>类如下,对应的代码位于<code class="language-plaintext highlighter-rouge">statement.go</code>文件中,<code class="language-plaintext highlighter-rouge">prepare statement</code>是通过调用<code class="language-plaintext highlighter-rouge">mysqlConn</code>的<code class="language-plaintext highlighter-rouge">prepare</code>方法开启的,对应流程及源码如下:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">mysqlStmt</span> <span class="k">struct</span> <span class="p">{</span>
<span class="c">// 关联的 mysql 连接</span>
<span class="n">mc</span> <span class="o">*</span><span class="n">mysqlConn</span>
<span class="c">// 预处理语句的标识 id</span>
<span class="n">id</span> <span class="kt">uint32</span>
<span class="c">// 预处理状态中多少待填充参数</span>
<span class="n">paramCount</span> <span class="kt">int</span>
<span class="p">}</span>
</code></pre></div></div>
<div>
<img src="https://pic2.zhimg.com/v2-7b43f2a660db9bad53ec4314e2ba704d_1440w.jpg" width="580" />
</div>
<h3 id="gorm框架原理分析"><code class="language-plaintext highlighter-rouge">gorm</code>框架原理分析</h3>
<p><code class="language-plaintext highlighter-rouge">gorm</code>框架通过一个<code class="language-plaintext highlighter-rouge">gorm.DB</code>实例来指代我们所操作的数据库. 使用<code class="language-plaintext highlighter-rouge">gorm</code>的第一步就是要通过<code class="language-plaintext highlighter-rouge">Open</code>方法创建出一个<code class="language-plaintext highlighter-rouge">gorm.DB</code>实例,其中首个入参为连接器<code class="language-plaintext highlighter-rouge">dialector</code>,本身是个抽象的<code class="language-plaintext highlighter-rouge">interface</code>,其实现类关联了具体数据库类型.</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">(</span>
<span class="s">"gorm.io/driver/mysql"</span>
<span class="s">"gorm.io/gorm"</span>
<span class="p">)</span>
<span class="k">var</span> <span class="p">(</span>
<span class="n">dsn</span> <span class="o">:=</span> <span class="s">"root:123456@tcp(127.0.0.1:3306)/douban_datahub?charset=utf8mb4&parseTime=True&loc=Local"</span>
<span class="n">db</span> <span class="o">*</span><span class="n">gorm</span><span class="o">.</span><span class="n">DB</span>
<span class="n">dbOnce</span> <span class="n">sync</span><span class="o">.</span><span class="n">Once</span>
<span class="p">)</span>
<span class="k">func</span> <span class="n">getDB</span><span class="p">()</span> <span class="p">(</span><span class="o">*</span><span class="n">gorm</span><span class="o">.</span><span class="n">DB</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="k">var</span> <span class="n">err</span> <span class="kt">error</span>
<span class="n">dbOnce</span><span class="o">.</span><span class="n">Do</span><span class="p">(</span><span class="k">func</span><span class="p">(){</span>
<span class="c">// 创建 db 实例</span>
<span class="n">db</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">gorm</span><span class="o">.</span><span class="n">Open</span><span class="p">(</span><span class="n">mysql</span><span class="o">.</span><span class="n">Open</span><span class="p">(</span><span class="n">dsn</span><span class="p">),</span><span class="o">&</span><span class="n">gorm</span><span class="o">.</span><span class="n">Config</span><span class="p">{})</span>
<span class="p">})</span>
<span class="k">return</span> <span class="n">db</span><span class="p">,</span><span class="n">err</span>
<span class="p">}</span>
</code></pre></div></div>
<p><strong>创建gorm.DB</strong>实例流程,<code class="language-plaintext highlighter-rouge">gorm.Open</code>方法是创建<code class="language-plaintext highlighter-rouge">DB</code>实例的入口方法,其中包含如下几项核心步骤:</p>
<ul>
<li>完成<code class="language-plaintext highlighter-rouge">gorm.Config</code>配置的创建和注入,完成连接器<code class="language-plaintext highlighter-rouge">dialector</code>的注入,本篇使用的是<code class="language-plaintext highlighter-rouge">mysql</code>版本;</li>
<li>完成<code class="language-plaintext highlighter-rouge">callbacks</code>中<code class="language-plaintext highlighter-rouge">crud</code>等几类<code class="language-plaintext highlighter-rouge">processor</code>的创建 (通过<code class="language-plaintext highlighter-rouge">initializeCallbacks(...) </code>方法 )</li>
<li>完成<code class="language-plaintext highlighter-rouge">connPool</code>的创建以及各类<code class="language-plaintext highlighter-rouge">processor fns</code>函数的注册(通过<code class="language-plaintext highlighter-rouge">dialector.Initialize(...)</code>方法)</li>
<li>倘若启用了<code class="language-plaintext highlighter-rouge">prepare</code>模式,需要使用<code class="language-plaintext highlighter-rouge">preparedStmtDB</code>进行<code class="language-plaintext highlighter-rouge">connPool</code>的平替 ,构造<code class="language-plaintext highlighter-rouge">statement</code>实例</li>
<li>根据策略,决定是否通过<code class="language-plaintext highlighter-rouge">ping</code>请求测试连接,返回创建好的<code class="language-plaintext highlighter-rouge">db</code>实例;
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Open initialize db session based on dialector</span>
<span class="k">func</span> <span class="n">Open</span><span class="p">(</span><span class="n">dialector</span> <span class="n">Dialector</span><span class="p">,</span> <span class="n">opts</span> <span class="o">...</span><span class="n">Option</span><span class="p">)</span> <span class="p">(</span><span class="n">db</span> <span class="o">*</span><span class="n">DB</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">config</span> <span class="o">:=</span> <span class="o">&</span><span class="n">Config</span><span class="p">{}</span>
<span class="c">// ...</span>
<span class="k">if</span> <span class="n">config</span><span class="o">.</span><span class="n">NamingStrategy</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span> <span class="c">// 表、列命名策略</span>
<span class="n">config</span><span class="o">.</span><span class="n">NamingStrategy</span> <span class="o">=</span> <span class="n">schema</span><span class="o">.</span><span class="n">NamingStrategy</span><span class="p">{</span><span class="n">IdentifierMaxLength</span><span class="o">:</span> <span class="m">64</span><span class="p">}</span> <span class="c">// Default Identifier length is 64</span>
<span class="p">}</span>
<span class="k">if</span> <span class="n">dialector</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="c">// 连接器</span>
<span class="n">config</span><span class="o">.</span><span class="n">Dialector</span> <span class="o">=</span> <span class="n">dialector</span>
<span class="p">}</span>
<span class="n">db</span> <span class="o">=</span> <span class="o">&</span><span class="n">DB</span><span class="p">{</span><span class="n">Config</span><span class="o">:</span> <span class="n">config</span><span class="p">,</span> <span class="n">clone</span><span class="o">:</span> <span class="m">1</span><span class="p">}</span>
<span class="n">db</span><span class="o">.</span><span class="n">callbacks</span> <span class="o">=</span> <span class="n">initializeCallbacks</span><span class="p">(</span><span class="n">db</span><span class="p">)</span> <span class="c">// 初始化 callback 当中的各个 processor</span>
<span class="k">if</span> <span class="n">config</span><span class="o">.</span><span class="n">PrepareStmt</span> <span class="p">{</span> <span class="c">// 是否启用 prepare 模式</span>
<span class="n">preparedStmt</span> <span class="o">:=</span> <span class="n">NewPreparedStmtDB</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">ConnPool</span><span class="p">)</span>
<span class="n">db</span><span class="o">.</span><span class="n">cacheStore</span><span class="o">.</span><span class="n">Store</span><span class="p">(</span><span class="n">preparedStmtDBKey</span><span class="p">,</span> <span class="n">preparedStmt</span><span class="p">)</span>
<span class="n">db</span><span class="o">.</span><span class="n">ConnPool</span> <span class="o">=</span> <span class="n">preparedStmt</span>
<span class="p">}</span>
<span class="c">// ...</span>
<span class="p">}</span>
</code></pre></div> </div>
</li>
</ul>
<div>
<img src="https://pica.zhimg.com/v2-7d40d77a341f916d9a5510c1cc4dcc0c_1440w.jpg" width="580" />
</div>
<p><strong>初始化dialector</strong>,<code class="language-plaintext highlighter-rouge">gorm</code>中<code class="language-plaintext highlighter-rouge">mysql</code>版本的<code class="language-plaintext highlighter-rouge">dialector</code>实现在代码仓库 https://github.com/go-gorm/mysql 中,使用者通过<code class="language-plaintext highlighter-rouge">Open</code>方法,将传入的<code class="language-plaintext highlighter-rouge">dsn</code>解析成配置,然后返回<code class="language-plaintext highlighter-rouge">mysql</code>版本的<code class="language-plaintext highlighter-rouge">Dialector</code>实例.</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// go-gorm/mysql/mysql.go</span>
<span class="k">func</span> <span class="n">Open</span><span class="p">(</span><span class="n">dsn</span> <span class="kt">string</span><span class="p">)</span> <span class="n">gorm</span><span class="o">.</span><span class="n">Dialector</span> <span class="p">{</span>
<span class="n">dsnConf</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">mysql</span><span class="o">.</span><span class="n">ParseDSN</span><span class="p">(</span><span class="n">dsn</span><span class="p">)</span>
<span class="k">return</span> <span class="o">&</span><span class="n">Dialector</span><span class="p">{</span><span class="n">Config</span><span class="o">:</span> <span class="o">&</span><span class="n">Config</span><span class="p">{</span><span class="n">DSN</span><span class="o">:</span> <span class="n">dsn</span><span class="p">,</span> <span class="n">DSNConfig</span><span class="o">:</span> <span class="n">dsnConf</span><span class="p">}}</span>
<span class="p">}</span>
<span class="c">// 在gorm.Open中,当dialector不为空时,会调用config.Dialector.Initialize(db),对应实现在 go-gorm/mysql/mysql.go中</span>
<span class="k">func</span> <span class="p">(</span><span class="n">dialector</span> <span class="n">Dialector</span><span class="p">)</span> <span class="n">Initialize</span><span class="p">(</span><span class="n">db</span> <span class="o">*</span><span class="n">gorm</span><span class="o">.</span><span class="n">DB</span><span class="p">)</span> <span class="p">(</span><span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">dialector</span><span class="o">.</span><span class="n">DriverName</span> <span class="o">==</span> <span class="s">""</span> <span class="p">{</span>
<span class="n">dialector</span><span class="o">.</span><span class="n">DriverName</span> <span class="o">=</span> <span class="n">DefaultDriverName</span>
<span class="p">}</span>
<span class="c">// connPool 初始化</span>
<span class="k">if</span> <span class="n">dialector</span><span class="o">.</span><span class="n">Conn</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">db</span><span class="o">.</span><span class="n">ConnPool</span> <span class="o">=</span> <span class="n">dialector</span><span class="o">.</span><span class="n">Conn</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">db</span><span class="o">.</span><span class="n">ConnPool</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">sql</span><span class="o">.</span><span class="n">Open</span><span class="p">(</span><span class="n">dialector</span><span class="o">.</span><span class="n">DriverName</span><span class="p">,</span> <span class="n">dialector</span><span class="o">.</span><span class="n">DSN</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">err</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c">// register callbacks</span>
<span class="n">callbackConfig</span> <span class="o">:=</span> <span class="o">&</span><span class="n">callbacks</span><span class="o">.</span><span class="n">Config</span><span class="p">{</span>
<span class="n">CreateClauses</span><span class="o">:</span> <span class="n">CreateClauses</span><span class="p">,</span>
<span class="n">QueryClauses</span><span class="o">:</span> <span class="n">QueryClauses</span><span class="p">,</span>
<span class="n">UpdateClauses</span><span class="o">:</span> <span class="n">UpdateClauses</span><span class="p">,</span>
<span class="n">DeleteClauses</span><span class="o">:</span> <span class="n">DeleteClauses</span><span class="p">,</span>
<span class="p">}</span>
<span class="c">// ...完成 crud 类操作 callback 函数的注册</span>
<span class="n">callbacks</span><span class="o">.</span><span class="n">RegisterDefaultCallbacks</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">callbackConfig</span><span class="p">)</span>
<span class="k">return</span>
<span class="p">}</span>
</code></pre></div></div>
<p><strong>查询</strong>,以<code class="language-plaintext highlighter-rouge">db.First</code>方法作为入口,展示数据库查询的方法链路,在<code class="language-plaintext highlighter-rouge">db.First</code>方法当中:</p>
<ul>
<li>遵循<code class="language-plaintext highlighter-rouge">First</code>的语义,通过<code class="language-plaintext highlighter-rouge">limit</code>和<code class="language-plaintext highlighter-rouge">order</code>追加<code class="language-plaintext highlighter-rouge">clause</code>,限制只取满足条件且主键最小的一笔数据;</li>
<li>追加用户传入的一系列<code class="language-plaintext highlighter-rouge">condition</code>,进行<code class="language-plaintext highlighter-rouge">clause</code>追加;</li>
<li>在<code class="language-plaintext highlighter-rouge">First</code>、<code class="language-plaintext highlighter-rouge">Take</code>、<code class="language-plaintext highlighter-rouge">Last</code>等方法中,会设置<code class="language-plaintext highlighter-rouge">RaiseErrorOnNotFound</code>标识为<code class="language-plaintext highlighter-rouge">true</code>,倘若未找到记录,则会抛出<code class="language-plaintext highlighter-rouge">ErrRecordNotFound</code>错误;</li>
</ul>
<div>
<img src="https://pic3.zhimg.com/v2-2315bd553cb5e3cb1896a67498635b9a_1440w.jpg" width="580" />
</div>
<p><strong>添加条件</strong>,执行查询类操作时,通常会通过链式调用的方式,传入一些查询限制条件,比如 Where、Group By、Order、Limit 之类. 我们以 Limit 为例,进行展开介绍:</p>
<ul>
<li>首先调用 db.getInstance() 方法,克隆出一份 DB 会话实例</li>
<li>调用 statement.AddClause 方法,将 limit 条件追加到 statement 的 Clauses map 中
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">db</span> <span class="o">*</span><span class="n">DB</span><span class="p">)</span> <span class="n">Limit</span><span class="p">(</span><span class="n">limit</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="n">tx</span> <span class="o">*</span><span class="n">DB</span><span class="p">)</span> <span class="p">{</span>
<span class="n">tx</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">getInstance</span><span class="p">()</span>
<span class="n">tx</span><span class="o">.</span><span class="n">Statement</span><span class="o">.</span><span class="n">AddClause</span><span class="p">(</span><span class="n">clause</span><span class="o">.</span><span class="n">Limit</span><span class="p">{</span><span class="n">Limit</span><span class="o">:</span> <span class="o">&</span><span class="n">limit</span><span class="p">})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">stmt</span> <span class="o">*</span><span class="n">Statement</span><span class="p">)</span> <span class="n">AddClause</span><span class="p">(</span><span class="n">v</span> <span class="n">clause</span><span class="o">.</span><span class="n">Interface</span><span class="p">)</span> <span class="p">{</span>
<span class="n">name</span> <span class="o">:=</span> <span class="n">v</span><span class="o">.</span><span class="n">Name</span><span class="p">()</span>
<span class="n">c</span> <span class="o">:=</span> <span class="n">stmt</span><span class="o">.</span><span class="n">Clauses</span><span class="p">[</span><span class="n">name</span><span class="p">]</span>
<span class="n">c</span><span class="o">.</span><span class="n">Name</span> <span class="o">=</span> <span class="n">name</span>
<span class="n">v</span><span class="o">.</span><span class="n">MergeClause</span><span class="p">(</span><span class="o">&</span><span class="n">c</span><span class="p">)</span>
<span class="n">stmt</span><span class="o">.</span><span class="n">Clauses</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">c</span>
<span class="p">}</span>
</code></pre></div> </div>
</li>
</ul>
<div>
<img src="https://pic2.zhimg.com/v2-af6ba84f71fe483207f1a21091d68a61_1440w.jpg" width="580" />
</div>
Elasticsearch核心技术与实战
2022-11-01T00:00:00+00:00
https://dongma.github.io/2022/11/01/elasticsearch
<p>在极客时间上学习<code class="language-plaintext highlighter-rouge">elasticsearch</code>课程,主要关注点在<code class="language-plaintext highlighter-rouge">query</code>的<code class="language-plaintext highlighter-rouge">DSL</code>语句以及集群的管理,在本地基于<code class="language-plaintext highlighter-rouge">es 7.1</code>来构建集群服务,启动脚本如下,同时在<code class="language-plaintext highlighter-rouge">conf/elasticsearch.yml</code>中添加<code class="language-plaintext highlighter-rouge">xpack.ml.enabled: false</code>、<code class="language-plaintext highlighter-rouge">http.host: 0.0.0.0</code>的配置(禁用<code class="language-plaintext highlighter-rouge">ml</code>及启用<code class="language-plaintext highlighter-rouge">host</code>):</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bash> bin/elasticsearch <span class="nt">-E</span> node.name<span class="o">=</span>node0 <span class="nt">-E</span> cluster.name<span class="o">=</span>geektime <span class="nt">-E</span> path.data<span class="o">=</span>node0_data <span class="nt">-d</span>
bash> bin/elasticsearch <span class="nt">-E</span> node.name<span class="o">=</span>node1 <span class="nt">-E</span> cluster.name<span class="o">=</span>geektime <span class="nt">-E</span> path.data<span class="o">=</span>node1_data <span class="nt">-d</span>
bash> bin/elasticsearch <span class="nt">-E</span> node.name<span class="o">=</span>node2 <span class="nt">-E</span> cluster.name<span class="o">=</span>geektime <span class="nt">-E</span> path.data<span class="o">=</span>node2_data <span class="nt">-d</span>
bash> bin/elasticsearch <span class="nt">-E</span> node.name<span class="o">=</span>node3 <span class="nt">-E</span> cluster.name<span class="o">=</span>geektime <span class="nt">-E</span> path.data<span class="o">=</span>node3_data <span class="nt">-d</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">docker</code>容器中启动<code class="language-plaintext highlighter-rouge">cerebro</code>服务,用于监控<code class="language-plaintext highlighter-rouge">elasticsearch</code>集群的状态,<code class="language-plaintext highlighter-rouge">docker</code>启动命令如下:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bash> docker run <span class="nt">-d</span> <span class="nt">--name</span> cerebro <span class="nt">-p</span> 9100:9000 lmenezes/cerebro:latest
</code></pre></div></div>
<!-- more -->
<h2 id="文档index基础操作">文档index基础操作</h2>
<p>1) <code class="language-plaintext highlighter-rouge">elasticsearch</code>中创建新文档,用<code class="language-plaintext highlighter-rouge">post</code>请求方式,<code class="language-plaintext highlighter-rouge">url</code>内容为<code class="language-plaintext highlighter-rouge">index/_doc/id</code>。当未指定{id}时,会自动生成随机的<code class="language-plaintext highlighter-rouge">id</code>。<code class="language-plaintext highlighter-rouge">put</code>方式用于更新文档,当<code class="language-plaintext highlighter-rouge">PUT users/_doc/1?op_type=create</code>或<code class="language-plaintext highlighter-rouge">PUT users/_create/1</code>指定文档<code class="language-plaintext highlighter-rouge">id</code>存在时,就会报错。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST <span class="nb">users</span>/_doc
<span class="o">{</span>
<span class="s2">"user"</span>: <span class="s2">"mike"</span>,
<span class="s2">"post_date"</span>: <span class="s2">"2019-04-15T14:12:12"</span>,
<span class="s2">"message"</span>: <span class="s2">"trying out kibana"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>2) <code class="language-plaintext highlighter-rouge">elasticsearch</code>的分词器<code class="language-plaintext highlighter-rouge">analysis</code>,分词是指把全文本转换为一些列的单词<code class="language-plaintext highlighter-rouge">(term/token)</code>的过程,其通常由<code class="language-plaintext highlighter-rouge">Character Filters</code>、<code class="language-plaintext highlighter-rouge">Tokenizer</code>、<code class="language-plaintext highlighter-rouge">Token Filters</code>这三部分组成。具体<code class="language-plaintext highlighter-rouge">url</code>示例如下,<code class="language-plaintext highlighter-rouge">analyzer</code>的类型可以有:<code class="language-plaintext highlighter-rouge">standard</code>、<code class="language-plaintext highlighter-rouge">stop</code>、<code class="language-plaintext highlighter-rouge">simple</code>等。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET _analyze
<span class="o">{</span>
<span class="s2">"analyzer"</span>: <span class="s2">"stop"</span>,
<span class="s2">"text"</span>: <span class="s2">"2 running Quick brown-foxes leap over lazy dogs in the summer evening."</span>
<span class="o">}</span>
</code></pre></div></div>
<p>3) <code class="language-plaintext highlighter-rouge">url</code>中<code class="language-plaintext highlighter-rouge">query string</code>的语法,指定字段v.s.泛查询,其中<code class="language-plaintext highlighter-rouge">df</code>为默认字段,当不指定<code class="language-plaintext highlighter-rouge">df</code>只按<code class="language-plaintext highlighter-rouge">q</code>查询时,则是泛查询,从<code class="language-plaintext highlighter-rouge">_doc</code>的所有字段检索:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /movies/_search?q<span class="o">=</span>2012&df<span class="o">=</span>title&sort<span class="o">=</span>year:desc&from<span class="o">=</span>0&size<span class="o">=</span>10&timeout<span class="o">=</span>1s
</code></pre></div></div>
<h2 id="url-searchrequest-body查询及文档mapping">URl Search、Request Body查询及文档Mapping</h2>
<p>1)在<code class="language-plaintext highlighter-rouge">elasticsearch</code>中查询可以分为<code class="language-plaintext highlighter-rouge">url search</code>和<code class="language-plaintext highlighter-rouge">request body</code>查询,其中<code class="language-plaintext highlighter-rouge">url search</code>用<code class="language-plaintext highlighter-rouge">GET</code>方式,相关参数放在<code class="language-plaintext highlighter-rouge">url</code>中。<code class="language-plaintext highlighter-rouge">df</code>指定默认查询字段,<code class="language-plaintext highlighter-rouge">q</code>为查询字符串。当未指定<code class="language-plaintext highlighter-rouge">df</code>时,称为泛查询,会拿数值与<code class="language-plaintext highlighter-rouge">doc</code>中所有字段进行匹配:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># es中查询的dsl,df指定默认字段,q为查询数值,TermQuery</span>
GET kibana_sample_data_ecommerce/_search?q<span class="o">=</span>Eddie&df<span class="o">=</span>customer_first_name
<span class="o">{</span>
<span class="s2">"profile"</span>: <span class="s2">"true"</span>
<span class="o">}</span>
<span class="c"># 若不用df的话,可以用q=field:value来进行替换</span>
GET kibana_sample_data_ecommerce/_search?q<span class="o">=</span>customer_first_name:Eddie
<span class="o">{</span>
<span class="s2">"profile"</span>: <span class="s2">"true"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>2)<code class="language-plaintext highlighter-rouge">Phrase query</code>与<code class="language-plaintext highlighter-rouge">Term query</code>的区别,<code class="language-plaintext highlighter-rouge">PhraseQuery</code>会按整个字符串进行匹配,而<code class="language-plaintext highlighter-rouge">TermQuery</code>则会对字符串进行分词。对于<code class="language-plaintext highlighter-rouge">term</code>来说,只要<code class="language-plaintext highlighter-rouge">Field value</code>中包含任意一个单词就可以。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># phrase query,相当于不会做分词,匹配完整字符串(1条)</span>
GET kibana_sample_data_ecommerce/_search?q<span class="o">=</span>customer_full_name:<span class="s2">"Eddie Underwood"</span>
<span class="o">{</span>
<span class="s2">"profile"</span>: <span class="s2">"true"</span>
<span class="o">}</span>
<span class="c"># term query,对字符串进行了分词,好像也有keyword概念,任意匹配Eddie或Underwood就可以</span>
GET kibana_sample_data_ecommerce/_search?q<span class="o">=</span>customer_full_name:Eddie Underwood
<span class="o">{</span>
<span class="s2">"profile"</span>: <span class="s2">"true"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>此外,在<code class="language-plaintext highlighter-rouge">url query</code>中还支持分组的概念,也就是<code class="language-plaintext highlighter-rouge">Bool Query</code>。当查询条件为<code class="language-plaintext highlighter-rouge">customer_full_name:(Eddie Underwood)</code>时,会分别按<code class="language-plaintext highlighter-rouge">Eddie</code>和<code class="language-plaintext highlighter-rouge">Underwood</code>进行匹配,其是任意的满足关系。若想在字段中同时满足要求,则可在分组中添加<code class="language-plaintext highlighter-rouge">AND</code>操作符。此外,<code class="language-plaintext highlighter-rouge">url query</code>还支持<code class="language-plaintext highlighter-rouge">range</code>查询及通配符查询。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># bool query,full_name中包括Eddie或Underwood才可以,实现同时包含,则需添加AND关键字</span>
GET kibana_sample_data_ecommerce/_search?q<span class="o">=</span>customer_full_name:<span class="o">(</span>Eddie AND Underwood<span class="o">)</span>
<span class="o">{</span>
<span class="s2">"profile"</span>: <span class="s2">"true"</span>
<span class="o">}</span>
<span class="c"># 数值范围查询,(订单总额)taxful_total_price大于50</span>
GET kibana_sample_data_ecommerce/_search?q<span class="o">=</span>taxful_total_price:><span class="o">=</span>50
<span class="o">{</span>
<span class="s2">"profile"</span>: <span class="s2">"true"</span>
<span class="o">}</span>
<span class="c"># 通配符查询,只要email字段中含"gwen"就会被匹配</span>
GET kibana_sample_data_ecommerce/_search?q<span class="o">=</span>email:gwen<span class="k">*</span>
<span class="o">{</span>
<span class="s2">"profile"</span>: <span class="s2">"true"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>3)<code class="language-plaintext highlighter-rouge">Request body</code>查询的详细解释,这其实是一种更通用的写法,使用<code class="language-plaintext highlighter-rouge">POST</code>请求方式。在<code class="language-plaintext highlighter-rouge">body</code>中使用<code class="language-plaintext highlighter-rouge">_source</code>指定要获取的字段列表,同时<code class="language-plaintext highlighter-rouge">sort</code>可指定按哪个字段进行排序。<code class="language-plaintext highlighter-rouge">query</code>部分指定了具体的查询条件,<code class="language-plaintext highlighter-rouge">operator</code>为<code class="language-plaintext highlighter-rouge">and</code>最终效果类似于<code class="language-plaintext highlighter-rouge">phrase query</code>。<code class="language-plaintext highlighter-rouge">elasticsearch</code>的<code class="language-plaintext highlighter-rouge">painless</code>脚本用于特定计算,返回计算后的新字段(如金额转换等)。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># es request body的写法,按订单总金额排序desc,_source过滤doc中的字段</span>
POST kibana_sample_data_ecommerce/_search
<span class="o">{</span>
<span class="s2">"_source"</span>: <span class="o">[</span><span class="s2">"taxful_total_price"</span>, <span class="s2">"total_quantity"</span>, <span class="s2">"customer_full_name"</span>, <span class="s2">"manufacturer"</span><span class="o">]</span>,
<span class="s2">"sort"</span>: <span class="o">[{</span><span class="s2">"taxful_total_price"</span>: <span class="s2">"desc"</span><span class="o">}]</span>,
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"match"</span>: <span class="o">{</span>
<span class="s2">"customer_full_name"</span>: <span class="o">{</span>
<span class="s2">"query"</span>: <span class="s2">"Eddie Lambert"</span>,
<span class="s2">"operator"</span>: <span class="s2">"and"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"script_fields"</span>: <span class="o">{</span>
<span class="s2">"addtional_field"</span>: <span class="o">{</span>
<span class="s2">"script"</span>: <span class="o">{</span>
<span class="s2">"lang"</span>: <span class="s2">"painless"</span>,
<span class="s2">"source"</span>: <span class="s2">"doc['taxful_total_price'].value + '_hello'"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>此外,对于<code class="language-plaintext highlighter-rouge">match_phrase</code>则不会进行分词,对<code class="language-plaintext highlighter-rouge">_doc</code>会直接进行查询。<code class="language-plaintext highlighter-rouge">body</code>中的<code class="language-plaintext highlighter-rouge">slop</code>参数可用于近似度查询,提升数据检索的容错性。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># match_phrase查询,不会进行分词,直接匹配total字符串,slop指定term结果</span>
POST kibana_sample_data_ecommerce/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"match_phrase"</span>: <span class="o">{</span>
<span class="s2">"customer_full_name"</span>: <span class="o">{</span>
<span class="s2">"query"</span>: <span class="s2">"Eddie Lambert"</span>,
<span class="s2">"slop"</span>: 1
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>4)<code class="language-plaintext highlighter-rouge">query_string</code>与<code class="language-plaintext highlighter-rouge">simple_query_string</code>的区别,<code class="language-plaintext highlighter-rouge">query_string</code>与<code class="language-plaintext highlighter-rouge">url query</code>类似,也需指定<code class="language-plaintext highlighter-rouge">default_field</code>。同时,其也支持多字段<code class="language-plaintext highlighter-rouge">fields</code>及多分组<code class="language-plaintext highlighter-rouge">query</code>的查询,<code class="language-plaintext highlighter-rouge">simple_query_string#query</code>也需指定查询条件。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># query_string和url query比较类似,也支持分组,如下的query_string#fields</span>
POST /users/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"query_string"</span>: <span class="o">{</span>
<span class="s2">"default_field"</span>: <span class="s2">"name"</span>,
<span class="s2">"query"</span>: <span class="s2">"Ruan AND YiMing"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
POST /users/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"query_string"</span>: <span class="o">{</span>
<span class="s2">"fields"</span>: <span class="o">[</span><span class="s2">"name"</span>, <span class="s2">"about"</span><span class="o">]</span>,
<span class="s2">"query"</span>: <span class="s2">"(Ruan And YiMing) OR (Java AND Elasticsearch)"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
POST /users/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"simple_query_string"</span>: <span class="o">{</span>
<span class="s2">"query"</span>: <span class="s2">"Ruan AND YiMing"</span>,
<span class="s2">"fields"</span>: <span class="o">[</span><span class="s2">"name"</span><span class="o">]</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>5)对于文档<code class="language-plaintext highlighter-rouge">mapping</code>这一部分,类似比喻的话,相当于是数据表的<code class="language-plaintext highlighter-rouge">schema</code>,规定了字段的约束信息。对于<code class="language-plaintext highlighter-rouge">dynamic mapping</code>,<code class="language-plaintext highlighter-rouge">elasticsearch</code>支持三种模式:<code class="language-plaintext highlighter-rouge">true</code>、<code class="language-plaintext highlighter-rouge">false</code>和<code class="language-plaintext highlighter-rouge">strict</code>。其默认值为<code class="language-plaintext highlighter-rouge">true</code>,当设置<code class="language-plaintext highlighter-rouge">mapping</code>为<code class="language-plaintext highlighter-rouge">false</code>时,新添加的字段不能检索,但会在<code class="language-plaintext highlighter-rouge">_source</code>部分展示,当为<code class="language-plaintext highlighter-rouge">strict</code>时,索引文档新增字段时,会进行报错。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET mapping_test/_mapping
<span class="c"># 修改dynamic为false,新加的字段不能被索引</span>
PUT dynamic_mapping_test/_mapping
<span class="o">{</span>
<span class="s2">"dynamic"</span>: <span class="nb">false</span>
<span class="o">}</span>
PUT dynamic_mapping_test/_doc/10
<span class="o">{</span>
<span class="s2">"anotherField"</span>: <span class="s2">"otherValue"</span>
<span class="o">}</span>
<span class="c"># dynamic为false时,新增的字段无法被检索,strict模式下,新添加字段会报错</span>
POST dynamic_mapping_test/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"match"</span>: <span class="o">{</span>
<span class="s2">"anotherField"</span>: <span class="s2">"otherValue"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="深入elasticsearch搜索机制">深入ElasticSearch搜索机制</h2>
<p>1)深入理解分词的逻辑,在使用<code class="language-plaintext highlighter-rouge">_bulk api</code>批量写入一批文档后,查询文档时,通过原有的字段是检索不到的,必须将其转换为小些。向<code class="language-plaintext highlighter-rouge">products</code>索引写入<code class="language-plaintext highlighter-rouge">3</code>条数据,分别为<code class="language-plaintext highlighter-rouge">Apple</code>的产品。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># _bulk api批量写入数据,一次写入3条数据</span>
POST /products/_bulk
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 1<span class="o">}}</span>
<span class="o">{</span><span class="s2">"productID"</span>: <span class="s2">"XHDK-1902-#fj3"</span>, <span class="s2">"desc"</span>: <span class="s2">"iPhone"</span>, <span class="s2">"price"</span>: 30<span class="o">}</span>
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 2<span class="o">}}</span>
<span class="o">{</span><span class="s2">"productID"</span>: <span class="s2">"XHDK-1003-#446"</span>, <span class="s2">"desc"</span>: <span class="s2">"iPad"</span>, <span class="s2">"price"</span>: 35<span class="o">}</span>
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 3<span class="o">}}</span>
<span class="o">{</span><span class="s2">"productID"</span>: <span class="s2">"XHDK-6902-#521"</span>, <span class="s2">"desc"</span>: <span class="s2">"MBP"</span>, <span class="s2">"price"</span>: 40<span class="o">}</span>
</code></pre></div></div>
<p>通过<code class="language-plaintext highlighter-rouge">term query</code>按<code class="language-plaintext highlighter-rouge">iPhone</code>进行检索时,是查不到数据的。原因是在存储文档时,<code class="language-plaintext highlighter-rouge">elasticsearch</code>对字段值进行了分词,数据字段按小写形式进行存储,当用<code class="language-plaintext highlighter-rouge">iphone</code>检索时是可以的。此外,<code class="language-plaintext highlighter-rouge">elasticsearch</code>中每个字段都有<code class="language-plaintext highlighter-rouge">keyword</code>属性,在用<code class="language-plaintext highlighter-rouge">field.keyword</code>查询时则可以进行完整的匹配。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 直接用iPhone在desc#value查询,搜不到记录。但用desc.keyword可以,因为在保存文档时,iPhone在索引中已进行了小写</span>
POST /products/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"term"</span>: <span class="o">{</span>
<span class="s2">"desc.keyword"</span>: <span class="o">{</span>
<span class="s2">"value"</span>: <span class="s2">"iPhone"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 将query改为filter的方式,忽略TF-IDF算分问题,避免相关性算分的开销,提升查询性能</span>
POST /products/_search
<span class="o">{</span>
<span class="s2">"explain"</span>: <span class="nb">true</span>,
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"constant_score"</span>: <span class="o">{</span>
<span class="s2">"filter"</span>: <span class="o">{</span>
<span class="s2">"term"</span>: <span class="o">{</span>
<span class="s2">"productID.keyword"</span>: <span class="s2">"XHDK-1902-#fj3"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>为了提升查询效率,可以用<code class="language-plaintext highlighter-rouge">constant_score#filter</code>来替换<code class="language-plaintext highlighter-rouge">term query</code>,因为其不进行算分,所以效率能高一些。同时,其也支持<code class="language-plaintext highlighter-rouge">range query</code>和<code class="language-plaintext highlighter-rouge">exists</code>操作符。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 用range方式进行范围查询,通过doc.price进行过滤</span>
GET /products/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"constant_score"</span>: <span class="o">{</span>
<span class="s2">"filter"</span>: <span class="o">{</span>
<span class="s2">"range"</span>: <span class="o">{</span>
<span class="s2">"price"</span>: <span class="o">{</span>
<span class="s2">"gte"</span>: 20, <span class="s2">"lte"</span>: 30
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 用exists来查找一些field值非空的文档,并将其进行返回</span>
POST /products/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"constant_score"</span>: <span class="o">{</span>
<span class="s2">"filter"</span>: <span class="o">{</span>
<span class="s2">"exists"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"desc"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>2)<code class="language-plaintext highlighter-rouge">query context</code>与<code class="language-plaintext highlighter-rouge">filter context</code>影响算分的问题,默认情况下<code class="language-plaintext highlighter-rouge">elasticsearch</code>会按照匹配度问题给文档进行打分,在文档每部分可使用<code class="language-plaintext highlighter-rouge">boost</code>来影响其分数,当文档中两个字段都含关键词时,可通过<code class="language-plaintext highlighter-rouge">boost</code>设置权重,进而影响文档的排名。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># query context与filter context影响算分问题</span>
POST /blogs/_bulk
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 1<span class="o">}}</span>
<span class="o">{</span><span class="s2">"title"</span>: <span class="s2">"Apple iPad"</span>, <span class="s2">"content"</span>: <span class="s2">"Apple iPad,Apple iPad"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 2<span class="o">}}</span>
<span class="o">{</span><span class="s2">"title"</span>: <span class="s2">"Apple iPad,Apple iPad"</span>, <span class="s2">"content"</span>: <span class="s2">"Apple iPad"</span><span class="o">}</span>
<span class="c"># 通过boost指定每部分字段的权重,进而影响文档的算分排序</span>
POST blogs/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"bool"</span>: <span class="o">{</span>
<span class="s2">"should"</span>: <span class="o">[</span>
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span>
<span class="s2">"title"</span>: <span class="o">{</span>
<span class="s2">"query"</span>: <span class="s2">"apple,ipad"</span>,
<span class="s2">"boost"</span>: 1
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span>
<span class="s2">"content"</span>: <span class="o">{</span>
<span class="s2">"query"</span>: <span class="s2">"apple,ipad"</span>,
<span class="s2">"boost"</span>: 2
<span class="o">}</span>
<span class="o">}}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">bool</code>查询中,<code class="language-plaintext highlighter-rouge">must</code>和<code class="language-plaintext highlighter-rouge">should</code>是算分的,而<code class="language-plaintext highlighter-rouge">must_not</code>则不计入算分,在检索示例中可通过<code class="language-plaintext highlighter-rouge">must</code>及<code class="language-plaintext highlighter-rouge">must_not</code>来过滤文档。默认情况下,用<code class="language-plaintext highlighter-rouge">term query</code>查询时,只要<code class="language-plaintext highlighter-rouge">doc</code>中包含关键字的频率高,则其相应的算分也会高。在具有相同数量关键词的字段中,<code class="language-plaintext highlighter-rouge">doc</code>长度越小的文档相关性越高。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 批量写入关于apple的新闻数据,批量写入文档记录</span>
POST news/_bulk
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 1<span class="o">}}</span>
<span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"Apple Mac"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 2<span class="o">}}</span>
<span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"Apple iPad"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 3<span class="o">}}</span>
<span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"Apple employee like Apple Pie and Apple Juice"</span><span class="o">}</span>
<span class="c"># 然而并不是所期望的,返回了apple食品记录</span>
POST news/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"bool"</span>: <span class="o">{</span>
<span class="s2">"must"</span>: <span class="o">{</span>
<span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"apple"</span><span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>可通过<code class="language-plaintext highlighter-rouge">must_not</code>对不符合条件的文档进行剔除,若只是想将不相关的文档分数减小,则可以通过<code class="language-plaintext highlighter-rouge">boosting#positive</code>或<code class="language-plaintext highlighter-rouge">boosting#negative</code>使得对文档进行重新的计分,这样不相关的文档也会进行展示,但其排名比较靠后。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 用must_not排除pie字符串,只剩余电子产品</span>
POST news/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"bool"</span>: <span class="o">{</span>
<span class="s2">"must"</span>: <span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"apple"</span><span class="o">}}</span>,
<span class="s2">"must_not"</span>: <span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"pie"</span><span class="o">}}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 当不想删除时,可使用boosting#positive、negative方式排序</span>
POST news/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"boosting"</span>: <span class="o">{</span>
<span class="s2">"positive"</span>: <span class="o">{</span>
<span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"apple"</span><span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"negative"</span>: <span class="o">{</span>
<span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"pie"</span><span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"negative_boost"</span>: 0.5
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>3)<code class="language-plaintext highlighter-rouge">disjunction query</code>也是关于文档相关性的,若文档中有两部分都匹配,若想按文档匹配度高的那一部分排序的话(不按累加求和),则应使用此查询。同时,还可按<code class="language-plaintext highlighter-rouge">tie_breaker</code>对文档分数进行扰乱,进而影响文档的排名。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PUT /blogs/_bulk
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 1<span class="o">}}</span>
<span class="o">{</span><span class="s2">"title"</span>: <span class="s2">"Quick brown rabbits"</span>, <span class="s2">"body"</span>: <span class="s2">"Brown rabbits are commonly seen"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"index"</span>: <span class="o">{</span><span class="s2">"_id"</span>: 2<span class="o">}}</span>
<span class="o">{</span><span class="s2">"title"</span>: <span class="s2">"Keeping pets happy"</span>, <span class="s2">"body"</span>: <span class="s2">"My quick brown fox eats rabbits on a regular basis."</span><span class="o">}</span>
<span class="c"># 用dis_max#queries找两部分,各自评分最高的内容,此外还可通过tie_breaker进行调整</span>
POST /blogs/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"dis_max"</span>: <span class="o">{</span>
<span class="s2">"queries"</span>: <span class="o">[</span>
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"title"</span>: <span class="s2">"Brown fox"</span><span class="o">}}</span>,
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"body"</span>: <span class="s2">"Brown fox"</span><span class="o">}}</span>
<span class="o">]</span>,
<span class="s2">"tie_breaker"</span>: 0.2
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>多字段查询的搜索语法,<code class="language-plaintext highlighter-rouge">most_fields</code>会累计多个字段的分数之和,<code class="language-plaintext highlighter-rouge">cross_fields</code>也就是当<code class="language-plaintext highlighter-rouge">query</code>在多个字段中存在时,就会返回结果,也就是所谓的跨字段查询。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PUT address/_doc/1
<span class="o">{</span>
<span class="s2">"street"</span>: <span class="s2">"5 Poland Street"</span>,
<span class="s2">"city"</span>: <span class="s2">"London"</span>,
<span class="s2">"country"</span>: <span class="s2">"United Kingdom"</span>,
<span class="s2">"postcode"</span>: <span class="s2">"W1V 3DG"</span>
<span class="o">}</span>
<span class="c"># 使用most_fields是可以的,但增加operator:and就不可以了。可将type改为cross_fields,表示将query string在多个字段中进行检索</span>
POST address/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"multi_match"</span>: <span class="o">{</span>
<span class="s2">"query"</span>: <span class="s2">"Poland Street W1V"</span>,
<span class="s2">"fields"</span>: <span class="o">[</span><span class="s2">"street"</span>, <span class="s2">"city"</span>, <span class="s2">"country"</span>, <span class="s2">"postcode"</span><span class="o">]</span>,
<span class="s2">"type"</span>: <span class="s2">"cross_fields"</span>,
<span class="s2">"operator"</span>: <span class="s2">"and"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>可以使用<code class="language-plaintext highlighter-rouge">alias</code>语法对索引进行重命名,应用场景多为<code class="language-plaintext highlighter-rouge">elasticsearch</code>索引数据备份,为避免应用服务端开发时修改配置,可做到无感数据源切换。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># index的alias操作,用于对address进行重命名</span>
POST _aliases
<span class="o">{</span>
<span class="s2">"actions"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"add"</span>: <span class="o">{</span>
<span class="s2">"index"</span>: <span class="s2">"address"</span>,
<span class="s2">"alias"</span>: <span class="s2">"address_latest"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="深入elasticsearch聚合分析">深入<code class="language-plaintext highlighter-rouge">ElasticSearch</code>聚合分析</h2>
<p><code class="language-plaintext highlighter-rouge">elasticsearch</code>聚合分<code class="language-plaintext highlighter-rouge">metric</code>和<code class="language-plaintext highlighter-rouge">bucket</code>两类,<code class="language-plaintext highlighter-rouge">metric</code>类似于一些指标(<code class="language-plaintext highlighter-rouge">count</code>、<code class="language-plaintext highlighter-rouge">avg</code>、<code class="language-plaintext highlighter-rouge">sum</code>等),而<code class="language-plaintext highlighter-rouge">bucket</code>相当于<code class="language-plaintext highlighter-rouge">sql</code>语句中的<code class="language-plaintext highlighter-rouge">group by</code>操作。</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="k">count</span><span class="p">(</span><span class="n">brand</span><span class="p">)</span><span class="o">=></span><span class="p">[</span><span class="n">metric</span><span class="p">]</span> <span class="k">from</span> <span class="n">cars</span> <span class="k">group</span> <span class="k">by</span> <span class="n">brand</span><span class="o">=></span><span class="p">[</span><span class="n">bucket</span><span class="p">];</span>
</code></pre></div></div>
<p>一个简单的例子,通过<code class="language-plaintext highlighter-rouge">elasticsearch</code>请求分别统计<code class="language-plaintext highlighter-rouge">max</code>、<code class="language-plaintext highlighter-rouge">min</code>和<code class="language-plaintext highlighter-rouge">avg</code>的平均工资,<code class="language-plaintext highlighter-rouge">size</code>设置为<code class="language-plaintext highlighter-rouge">0</code>表示不返回原始文档。<code class="language-plaintext highlighter-rouge">aggs</code>表示聚合语法开始,其中<code class="language-plaintext highlighter-rouge">max</code>、<code class="language-plaintext highlighter-rouge">min</code>为聚合类型,里面的<code class="language-plaintext highlighter-rouge">field</code>值<code class="language-plaintext highlighter-rouge">salary</code>表示要聚合的字段。其实,简化语法可直接用<code class="language-plaintext highlighter-rouge">stats</code>替换<code class="language-plaintext highlighter-rouge">max</code>,其在一次执行中会统计出相关指标。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Metrics聚合,找最低、最高及平均工资</span>
POST employees/_search
<span class="o">{</span>
<span class="s2">"size"</span>: 0,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"max_salary"</span>: <span class="o">{</span>
<span class="s2">"max"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"salary"</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"min_salary"</span>: <span class="o">{</span>
<span class="s2">"min"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"salary"</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"avg_salary"</span>: <span class="o">{</span>
<span class="s2">"avg"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"salary"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">elasticsearch</code>通过<code class="language-plaintext highlighter-rouge">jobs#terms</code>进行分桶操作,首先一点<code class="language-plaintext highlighter-rouge">elasticsearch</code>不能对<code class="language-plaintext highlighter-rouge">text</code>类型字段进行分桶(<code class="language-plaintext highlighter-rouge">keyword</code>是可以的),需打开<code class="language-plaintext highlighter-rouge">fielddata</code>的配置。<code class="language-plaintext highlighter-rouge">aggs</code>还可以嵌套,如下是对员工按<code class="language-plaintext highlighter-rouge">age</code>进行排序,并取前2位进行展示。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 对keyword进行聚合,必须要用.keyword,避免分词,直接用job会报错,还可指定terms#size参数</span>
POST employees/_search
<span class="o">{</span>
<span class="s2">"size"</span>: 0,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"jobs"</span>: <span class="o">{</span>
<span class="s2">"terms"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"job.keyword"</span>
<span class="o">}</span>,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"old_employee"</span>: <span class="o">{</span>
<span class="s2">"top_hits"</span>: <span class="o">{</span>
<span class="s2">"size"</span>: 2,
<span class="s2">"sort"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"age"</span>: <span class="o">{</span>
<span class="s2">"order"</span>: <span class="s2">"desc"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 对text字段打开fielddata,支持terms aggregation</span>
PUT employees/_mapping
<span class="o">{</span>
<span class="s2">"properties"</span>: <span class="o">{</span>
<span class="s2">"job"</span>: <span class="o">{</span>
<span class="s2">"type"</span>: <span class="s2">"text"</span>,
<span class="s2">"fielddata"</span>: <span class="s2">"true"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">cardinate</code>操作相当于<code class="language-plaintext highlighter-rouge">sql</code>中的<code class="language-plaintext highlighter-rouge">distinct count</code>操作,可用于去重后的计数。<code class="language-plaintext highlighter-rouge">salary</code>还支持按<code class="language-plaintext highlighter-rouge">range</code>进行数量查询,其中<code class="language-plaintext highlighter-rouge">key</code>的值可以进行自定义。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 对job.keyword进行聚合分析,cardinate操作,相当于做distinct count操作</span>
POST employees/_search
<span class="o">{</span>
<span class="s2">"size"</span>: 0,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"cardinate"</span>: <span class="o">{</span>
<span class="s2">"cardinality"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"job.keyword"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># salary range分桶,可以自定义桶#key,并按range进行查询</span>
POST employees/_search
<span class="o">{</span>
<span class="s2">"size"</span>: 0,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"salary_range"</span>: <span class="o">{</span>
<span class="s2">"range"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"salary"</span>,
<span class="s2">"ranges"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"to"</span>: 10000
<span class="o">}</span>,
<span class="o">{</span>
<span class="s2">"from"</span>: 10000,
<span class="s2">"to"</span>: 20000
<span class="o">}</span>,
<span class="o">{</span>
<span class="s2">"key"</span>: <span class="s2">">20000"</span>,
<span class="s2">"from"</span>: 20000
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">histogram</code>用于展示员工薪资的直方图,<code class="language-plaintext highlighter-rouge">field</code>表示按哪个字段展示,<code class="language-plaintext highlighter-rouge">interval</code>为直方图每格的间隔大小。此外,<code class="language-plaintext highlighter-rouge">elasticsearch</code>还支持<code class="language-plaintext highlighter-rouge">pipeline</code>操作,其会将<code class="language-plaintext highlighter-rouge">aggs</code>后的结果再进行分析,常见的有<code class="language-plaintext highlighter-rouge">min_bucket</code>、<code class="language-plaintext highlighter-rouge">max_bucket</code>、<code class="language-plaintext highlighter-rouge">avg_bucket</code>等操作。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># salary Histogram,工资分布的直方图</span>
POST /employees/_search
<span class="o">{</span>
<span class="s2">"size"</span>: 0,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"salary_histogram"</span>: <span class="o">{</span>
<span class="s2">"histogram"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"salary"</span>,
<span class="s2">"interval"</span>: 20000,
<span class="s2">"extended_bounds"</span>: <span class="o">{</span>
<span class="s2">"min"</span>: 0,
<span class="s2">"max"</span>: 100000
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># elasticsearch pipeline操作, min_bucket最终选出最低平均工资,max_bucket则求最大的工作类型,avg_bucket只是所有类型工作的平均值,percentiles_bucket为百分位数的统计</span>
POST /employees/_search
<span class="o">{</span>
<span class="s2">"size"</span>: 0,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"jobs"</span>: <span class="o">{</span>
<span class="s2">"terms"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"job.keyword"</span>,
<span class="s2">"size"</span>: 10
<span class="o">}</span>,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"avg_salary"</span>: <span class="o">{</span>
<span class="s2">"avg"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"salary"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"min_salary_by_jobs"</span>: <span class="o">{</span>
<span class="s2">"percentiles_bucket"</span>: <span class="o">{</span>
<span class="s2">"buckets_path"</span>: <span class="s2">"jobs>avg_salary"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Aggs Query</code>聚合的<code class="language-plaintext highlighter-rouge">filter</code>这块,共分为<code class="language-plaintext highlighter-rouge">Filter</code>、<code class="language-plaintext highlighter-rouge">Post_Filter</code>和<code class="language-plaintext highlighter-rouge">global</code>这<code class="language-plaintext highlighter-rouge">3</code>种类型,第一个在<code class="language-plaintext highlighter-rouge">aggs#old_person#filter</code>中,其行为属于前置<code class="language-plaintext highlighter-rouge">filter</code>(也即先过滤再<code class="language-plaintext highlighter-rouge">agg</code>)。第二个属于<code class="language-plaintext highlighter-rouge">post_aggs</code>,先进行<code class="language-plaintext highlighter-rouge">aggs</code>然后只展示<code class="language-plaintext highlighter-rouge">Dev Manager</code>的<code class="language-plaintext highlighter-rouge">bucket</code>桶。而<code class="language-plaintext highlighter-rouge">all#global{}</code>会排除<code class="language-plaintext highlighter-rouge">query#filter</code>的作用,而对所有<code class="language-plaintext highlighter-rouge">doc</code>进行计算。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Filter,先按age#from 从35岁开始filter</span>
POST employees/_search
<span class="o">{</span>
<span class="s2">"size"</span>: 0,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"old_person"</span>: <span class="o">{</span>
<span class="s2">"filter"</span>: <span class="o">{</span>
<span class="s2">"range"</span>: <span class="o">{</span>
<span class="s2">"age"</span>: <span class="o">{</span>
<span class="s2">"from"</span>: 35
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"jobs"</span>: <span class="o">{</span>
<span class="s2">"terms"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"job.keyword"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c">#post filter,相当于先做bucket分桶操作,然后再进行filter过滤</span>
POST /employees/_search
<span class="o">{</span>
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"jobs"</span>: <span class="o">{</span>
<span class="s2">"terms"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"job.keyword"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"post_filter"</span>: <span class="o">{</span>
<span class="s2">"match"</span>: <span class="o">{</span>
<span class="s2">"job.keyword"</span>: <span class="s2">"Dev Manager"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="elasticsearch数据建模"><code class="language-plaintext highlighter-rouge">ElasticSearch</code>数据建模</h2>
<p>数据建模-对象及<code class="language-plaintext highlighter-rouge">Nested</code>对象,例如<code class="language-plaintext highlighter-rouge">blog</code>文档中含<code class="language-plaintext highlighter-rouge">User</code>对象,结构类似于<code class="language-plaintext highlighter-rouge">json</code>。在用<code class="language-plaintext highlighter-rouge">Rest</code>接口进行查询时,可通过<code class="language-plaintext highlighter-rouge">user.username</code>进行嵌套式查询。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 插入一条blog信息, user为嵌套的对象,包含3个字段</span>
PUT nested_blog/_doc/1
<span class="o">{</span>
<span class="s2">"content"</span>: <span class="s2">"I like elasticsearch"</span>,
<span class="s2">"time"</span>: <span class="s2">"2022-11-06T00:00:00"</span>,
<span class="s2">"user"</span>: <span class="o">{</span>
<span class="s2">"userid"</span>: 1,
<span class="s2">"username"</span>: <span class="s2">"Jack"</span>,
<span class="s2">"city"</span>: <span class="s2">"ShangHai"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 查询blog的信息,对text做了分词,不区分大小写了</span>
POST nested_blog/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"bool"</span>: <span class="o">{</span>
<span class="s2">"must"</span>: <span class="o">[</span>
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"content"</span>: <span class="s2">"elasticsearch"</span><span class="o">}}</span>,
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"user.username"</span>: <span class="s2">"Jack"</span><span class="o">}}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>当嵌套字段类型为数组时,通过<code class="language-plaintext highlighter-rouge">bool</code>查询其返回的结果会存在异常。此时,<code class="language-plaintext highlighter-rouge">index</code>的<code class="language-plaintext highlighter-rouge">mapping</code>和查询的<code class="language-plaintext highlighter-rouge">dsl</code>也必须改为<code class="language-plaintext highlighter-rouge">nested query</code>。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 电影的mapping信息,对于数组类型字段,需将`type`改为`nested`</span>
PUT my_movies
<span class="o">{</span>
<span class="s2">"mappings"</span>: <span class="o">{</span>
<span class="s2">"properties"</span>: <span class="o">{</span>
<span class="s2">"actors"</span>: <span class="o">{</span>
<span class="s2">"type"</span>: <span class="s2">"nested"</span>,
<span class="s2">"properties"</span>: <span class="o">{</span><span class="s2">"first_name"</span>: <span class="o">{</span><span class="s2">"type"</span>: <span class="s2">"keyword"</span><span class="o">}</span>,
<span class="s2">"last_name"</span>: <span class="o">{</span><span class="s2">"type"</span>: <span class="s2">"keyword"</span><span class="o">}}</span>
<span class="o">}</span>,
<span class="s2">"title"</span>: <span class="o">{</span>
<span class="s2">"type"</span>: <span class="s2">"text"</span>,
<span class="s2">"fields"</span>: <span class="o">{</span><span class="s2">"keyword"</span>: <span class="o">{</span><span class="s2">"type"</span>: <span class="s2">"keyword"</span>, <span class="s2">"ignore_above"</span>: 256<span class="o">}}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 写入一条电影信息, actors部分为一个数组</span>
PUT my_movies/_doc/1
<span class="o">{</span>
<span class="s2">"title"</span>: <span class="s2">"Speed"</span>,
<span class="s2">"actors"</span>: <span class="o">[{</span><span class="s2">"first_name"</span>: <span class="s2">"Keanu"</span>, <span class="s2">"last_name"</span>: <span class="s2">"Reeves"</span><span class="o">}</span>,
<span class="o">{</span><span class="s2">"first_name"</span>: <span class="s2">"Dennis"</span>, <span class="s2">"last_name"</span>: <span class="s2">"Hopper"</span><span class="o">}]</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在进行数据检索时,<code class="language-plaintext highlighter-rouge">bool</code>类型的<code class="language-plaintext highlighter-rouge">query</code>,在<code class="language-plaintext highlighter-rouge">json</code>结构中也需指明<code class="language-plaintext highlighter-rouge">nested.path</code>,这样检索数据时,才会按同一个对象的<code class="language-plaintext highlighter-rouge">first_name</code>、<code class="language-plaintext highlighter-rouge">last_name</code>一起检索。此外,对于普通嵌套对象,<code class="language-plaintext highlighter-rouge">Agg</code>操作是不生效的。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 查询电影信息,但是检索到了结果,需调整为Nested Query, 再根据条件筛选就正确</span>
POST my_movies/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"bool"</span>: <span class="o">{</span>
<span class="s2">"must"</span>: <span class="o">[</span>
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"title"</span>: <span class="s2">"Speed"</span><span class="o">}}</span>,
<span class="o">{</span><span class="s2">"nested"</span>: <span class="o">{</span>
<span class="s2">"path"</span>: <span class="s2">"actors"</span>,
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"bool"</span>: <span class="o">{</span>
<span class="s2">"must"</span>: <span class="o">[</span>
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"actors.first_name"</span>: <span class="s2">"Keanu"</span><span class="o">}}</span>,
<span class="o">{</span><span class="s2">"match"</span>: <span class="o">{</span><span class="s2">"actors.last_name"</span>: <span class="s2">"Reeves"</span><span class="o">}}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 嵌套对象的Agg聚合操作,也需指定类型为Nested Query,普通Agg是不生效的</span>
POST my_movies/_search
<span class="o">{</span>
<span class="s2">"size"</span>: 0,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"actors"</span>: <span class="o">{</span>
<span class="s2">"nested"</span>: <span class="o">{</span>
<span class="s2">"path"</span>: <span class="s2">"actors"</span>
<span class="o">}</span>,
<span class="s2">"aggs"</span>: <span class="o">{</span>
<span class="s2">"actor_name"</span>: <span class="o">{</span>
<span class="s2">"terms"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"actors.first_name"</span>,
<span class="s2">"size"</span>: 10
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">elasticsearch</code>中的父子文档,索引的<code class="language-plaintext highlighter-rouge">mapping</code>如下所示,<code class="language-plaintext highlighter-rouge">blog_comments_relation#type</code>为<code class="language-plaintext highlighter-rouge">join</code>,在<code class="language-plaintext highlighter-rouge">relations</code>中定义了<code class="language-plaintext highlighter-rouge">blog</code>和<code class="language-plaintext highlighter-rouge">comment</code>的对应关系。在写入<code class="language-plaintext highlighter-rouge">blog</code>文档时,<code class="language-plaintext highlighter-rouge">blog_comments_relation#name</code>的值为<code class="language-plaintext highlighter-rouge">blog</code>。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Es中的父/子文档,blog_comments_relation#此part未看懂</span>
PUT my_blogs
<span class="o">{</span>
<span class="s2">"settings"</span>: <span class="o">{</span>
<span class="s2">"number_of_shards"</span>: 2
<span class="o">}</span>,
<span class="s2">"mappings"</span>: <span class="o">{</span>
<span class="s2">"properties"</span>: <span class="o">{</span>
<span class="s2">"blog_comments_relation"</span>: <span class="o">{</span>
<span class="s2">"type"</span>: <span class="s2">"join"</span>,
<span class="s2">"relations"</span>: <span class="o">{</span>
<span class="s2">"blog"</span>: <span class="s2">"comment"</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="s2">"content"</span>: <span class="o">{</span>
<span class="s2">"type"</span>: <span class="s2">"text"</span>
<span class="o">}</span>,
<span class="s2">"title"</span>: <span class="o">{</span>
<span class="s2">"type"</span>: <span class="s2">"keyword"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 索引父文档,分别写入两个文档</span>
PUT my_blogs/_doc/blog1
<span class="o">{</span>
<span class="s2">"title"</span>: <span class="s2">"Learning Elasticsearch"</span>,
<span class="s2">"content"</span>: <span class="s2">"Learning ELK @ geektime"</span>,
<span class="s2">"blog_comments_relation"</span>: <span class="o">{</span>
<span class="s2">"name"</span>: <span class="s2">"blog"</span>
<span class="o">}</span>
<span class="o">}</span>
PUT my_blogs/_doc/blog2
<span class="o">{</span>
<span class="s2">"title"</span>: <span class="s2">"Learning Hadoop"</span>,
<span class="s2">"content"</span>: <span class="s2">"Learning Hadoop"</span>,
<span class="s2">"blog_comments_relation"</span>: <span class="o">{</span>
<span class="s2">"name"</span>: <span class="s2">"blog"</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>索引<code class="language-plaintext highlighter-rouge">comment</code>子文档,需在<code class="language-plaintext highlighter-rouge">json</code>结构中指定<code class="language-plaintext highlighter-rouge">id</code>为<code class="language-plaintext highlighter-rouge">comment1</code>和<code class="language-plaintext highlighter-rouge">routing</code>信息,其中<code class="language-plaintext highlighter-rouge">index name</code>值为<code class="language-plaintext highlighter-rouge">comment</code>,对应的<code class="language-plaintext highlighter-rouge">parent</code>值为<code class="language-plaintext highlighter-rouge">blog1</code>。通过<code class="language-plaintext highlighter-rouge">my_blogs/_search</code>可以查到所有文档列表:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 索引子文档,需指定routing路由字段值</span>
PUT my_blogs/_doc/comment1?routing<span class="o">=</span>blog1
<span class="o">{</span>
<span class="s2">"comment"</span>: <span class="s2">"I am learning ELk"</span>,
<span class="s2">"username"</span>: <span class="s2">"Jack"</span>,
<span class="s2">"blog_comments_relation"</span>: <span class="o">{</span>
<span class="s2">"name"</span>: <span class="s2">"comment"</span>,
<span class="s2">"parent"</span>: <span class="s2">"blog1"</span>
<span class="o">}</span>
<span class="o">}</span>
PUT my_blogs/_doc/comment2?routing<span class="o">=</span>blog2
<span class="o">{</span>
<span class="s2">"comment"</span>: <span class="s2">"I like Hadoop !!!"</span>,
<span class="s2">"username"</span>: <span class="s2">"Jack"</span>,
<span class="s2">"blog_comments_relation"</span>: <span class="o">{</span>
<span class="s2">"name"</span>: <span class="s2">"comment"</span>,
<span class="s2">"parent"</span>: <span class="s2">"blog2"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 查询所有文档,包含blog和comment两种类型</span>
POST my_blogs/_search
<span class="o">{}</span>
</code></pre></div></div>
<p>父子文档间的查询,通过父文档<code class="language-plaintext highlighter-rouge">id</code>查询,若查看blog#comment,则可以通过<code class="language-plaintext highlighter-rouge">parent_id</code>来查询,其中<code class="language-plaintext highlighter-rouge">type</code>值为<code class="language-plaintext highlighter-rouge">comment</code>。若想根据<code class="language-plaintext highlighter-rouge">comment</code>查询对应的<code class="language-plaintext highlighter-rouge">blog</code>,则可使用<code class="language-plaintext highlighter-rouge">has_child</code>注解。此外,可通过<code class="language-plaintext highlighter-rouge">comment2</code>和<code class="language-plaintext highlighter-rouge">routing</code>查看<code class="language-plaintext highlighter-rouge">blog2</code>下所有的评论数据。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 根据父文档id查询</span>
GET my_blogs/_doc/blog2
<span class="c"># parentId查询,依据blog2查到其下所有comment</span>
POST my_blogs/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"parent_id"</span>: <span class="o">{</span>
<span class="s2">"type"</span>: <span class="s2">"comment"</span>,
<span class="s2">"id"</span>: <span class="s2">"blog2"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># has child查询返回父文档, has parent查询会返回子文档</span>
POST my_blogs/_search
<span class="o">{</span>
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"has_child"</span>: <span class="o">{</span>
<span class="s2">"type"</span>: <span class="s2">"comment"</span>,
<span class="s2">"query"</span>: <span class="o">{</span>
<span class="s2">"match"</span>: <span class="o">{</span>
<span class="s2">"username"</span>: <span class="s2">"Jack"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c"># 通过id和routing来访问子文档</span>
GET my_blogs/_doc/comment2?routing<span class="o">=</span>blog2
</code></pre></div></div>
<p>对于<code class="language-plaintext highlighter-rouge">elasticsearch</code>中已有的<code class="language-plaintext highlighter-rouge">index</code>,要修改其某个字段类型时,只能对当前索引进行<code class="language-plaintext highlighter-rouge">reindex</code>操作。直接更新索引<code class="language-plaintext highlighter-rouge">mapping</code>文件,会抛出<code class="language-plaintext highlighter-rouge">remote_transport_exception</code>的异常。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># reindex api,类似于导数据</span>
POST _reindex
<span class="o">{</span>
<span class="s2">"source"</span>: <span class="o">{</span>
<span class="s2">"index"</span>: <span class="s2">"reindex_blogs"</span>
<span class="o">}</span>,
<span class="s2">"dest"</span>: <span class="o">{</span>
<span class="s2">"index"</span>: <span class="s2">"blogs_fix"</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">elasticsearch</code>中<code class="language-plaintext highlighter-rouge">pipeline</code>和<code class="language-plaintext highlighter-rouge">painless</code>脚本,可通过<code class="language-plaintext highlighter-rouge">PUT</code>请求直接注册一个<code class="language-plaintext highlighter-rouge">blog_pipeline</code>,<code class="language-plaintext highlighter-rouge">processors</code>可以有多种类型,像<code class="language-plaintext highlighter-rouge">split</code>会对指定字段进行切分,并且指定切分字符串为<code class="language-plaintext highlighter-rouge">,</code>。在索引文档时,可以指定<code class="language-plaintext highlighter-rouge">blog_pipeline</code>,这样存入文档的字段会被切分开。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 为ES增加一个pipeline, 对index的文档进行计算</span>
PUT _ingest/pipeline/blog_pipeline
<span class="o">{</span>
<span class="s2">"description"</span>: <span class="s2">"a blog pipeline"</span>,
<span class="s2">"processors"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"split"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"tags"</span>,
<span class="s2">"separator"</span>: <span class="s2">","</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="o">{</span>
<span class="s2">"set"</span>: <span class="o">{</span>
<span class="s2">"field"</span>: <span class="s2">"views"</span>,
<span class="s2">"value"</span>: 0
<span class="o">}</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="c"># 测试pipeline,确实tags字段被切分了,同时增加了views字段</span>
POST _ingest/pipeline/blog_pipeline/_simulate
<span class="o">{</span>
<span class="s2">"docs"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"_source"</span>: <span class="o">{</span>
<span class="s2">"title"</span>: <span class="s2">"Introducing big data...."</span>,
<span class="s2">"tags"</span>: <span class="s2">"openstask,k8s"</span>,
<span class="s2">"content"</span>: <span class="s2">"you known, for cloud"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
PUT tech_blogs/_doc/2?pipeline<span class="o">=</span>blog_pipeline
<span class="o">{</span>
<span class="s2">"title"</span>: <span class="s2">"Introducing big data...."</span>,
<span class="s2">"tags"</span>: <span class="s2">"openstask,k8s"</span>,
<span class="s2">"content"</span>: <span class="s2">"you known, for cloud"</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">painless</code>脚本内容如下,在<code class="language-plaintext highlighter-rouge">script</code>语法中指定执行脚本,其中<code class="language-plaintext highlighter-rouge">ctx</code>可取上下文中定义的对象。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST tech_blogs/_update/1
<span class="o">{</span>
<span class="s2">"script"</span>: <span class="o">{</span>
<span class="s2">"source"</span>: <span class="s2">"ctx._source.views += params.views"</span>,
<span class="s2">"params"</span>: <span class="o">{</span>
<span class="s2">"views"</span>: 100
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
大数据时代数据仓库Hive
2022-01-22T00:00:00+00:00
https://dongma.github.io/2022/01/22/apache-hive
<p>在<code class="language-plaintext highlighter-rouge">Hadoop</code>大数据平台及生态系统中,使用<code class="language-plaintext highlighter-rouge">mapreduce</code>模型进行编程,对广大用户来说,仍然是具有挑战性的任务。人们希望使用熟悉的<code class="language-plaintext highlighter-rouge">SQL</code>语言,对<code class="language-plaintext highlighter-rouge">hadoop</code>平台上的数据进行分析处理,这就是<code class="language-plaintext highlighter-rouge">SQL On Hadoop</code>系统诞的背景。</p>
<p><code class="language-plaintext highlighter-rouge">SQL on Hadoop</code>是一类系统的简称,这类系统利用<code class="language-plaintext highlighter-rouge">Hadoop</code>实现大量数据的管理,具体是利用<code class="language-plaintext highlighter-rouge">HDFS</code>实现高度可扩展的数据存储。在<code class="language-plaintext highlighter-rouge">HDFS</code>之上,实现<code class="language-plaintext highlighter-rouge">SQL</code>的查询引擎,使得用户可以使用<code class="language-plaintext highlighter-rouge">SQL</code>语言,对存储在<code class="language-plaintext highlighter-rouge">HDFS</code>上的数据进行分析。</p>
<h3 id="apache-hive的产生"><code class="language-plaintext highlighter-rouge">Apache Hive</code>的产生</h3>
<p><code class="language-plaintext highlighter-rouge">Hive</code>是基于<code class="language-plaintext highlighter-rouge">Hadoop</code>的一个数仓工具,可以将结构化的数据文件映射为一张数据库表,并提供简单的类<code class="language-plaintext highlighter-rouge">SQL(HQL)</code>查询功能,可以将<code class="language-plaintext highlighter-rouge">HQL</code>语句转换成为<code class="language-plaintext highlighter-rouge">MapReduce</code>任务进行运行。使用类<code class="language-plaintext highlighter-rouge">SQL</code>语句就可快速实现简单的<code class="language-plaintext highlighter-rouge">MapReduce</code>统计,不必开发专门的<code class="language-plaintext highlighter-rouge">MapReduce</code>应用。<code class="language-plaintext highlighter-rouge">Apache Hive</code>是由<code class="language-plaintext highlighter-rouge">Facebook</code>开发并开源,最后贡献给<code class="language-plaintext highlighter-rouge">Apache</code>基金会。</p>
<p><code class="language-plaintext highlighter-rouge">Hive</code>系统整体<code class="language-plaintext highlighter-rouge">3</code>个部分:用户接口、元数据存储、驱动器(<code class="language-plaintext highlighter-rouge">Driver</code>)在<code class="language-plaintext highlighter-rouge">Hadoop</code>上计算与存储。
<!-- more --></p>
<ol>
<li>用户接口主要有<code class="language-plaintext highlighter-rouge">3</code>个,<code class="language-plaintext highlighter-rouge">CLI</code>、<code class="language-plaintext highlighter-rouge">ThriftServer</code>和<code class="language-plaintext highlighter-rouge">HWI</code>。最常用的就是<code class="language-plaintext highlighter-rouge">CLI</code>,启动<code class="language-plaintext highlighter-rouge">hive</code>命令回同时启动一个<code class="language-plaintext highlighter-rouge">Hive Driver</code>。<code class="language-plaintext highlighter-rouge">ThriftServer</code>是以<code class="language-plaintext highlighter-rouge">Thrift</code>协议封装的<code class="language-plaintext highlighter-rouge">Hive</code>服务化接口,可提供跨语言的访问,如<code class="language-plaintext highlighter-rouge">Python</code>、<code class="language-plaintext highlighter-rouge">C++</code>等,最后一种是<code class="language-plaintext highlighter-rouge">Hive Web Interface</code>提供浏览器的访问方式。</li>
<li>表结构的一些<code class="language-plaintext highlighter-rouge">Meta</code>信息是存储在外部数据库的,如<code class="language-plaintext highlighter-rouge">MySQL</code>、<code class="language-plaintext highlighter-rouge">Oracle</code>和<code class="language-plaintext highlighter-rouge">Derby</code>库。<code class="language-plaintext highlighter-rouge">Hive</code>中元数据包括表的名字、表的列和分区及其属性、表的属性(是否为外部表等)、表的数据所在目录等。</li>
<li><code class="language-plaintext highlighter-rouge">Driver</code>部分包括:编译器、优化器和执行器,编译器完成词法分析、语法分析,将<code class="language-plaintext highlighter-rouge">HQL</code>转换为<code class="language-plaintext highlighter-rouge">AST</code>。<code class="language-plaintext highlighter-rouge">AST</code>生成逻辑执行计划,然后物理<code class="language-plaintext highlighter-rouge">MR</code>执行计划;优化器用来对逻辑计划、物理计划进行优化,生成的物理计划转变为<code class="language-plaintext highlighter-rouge">MR Job</code>并在<code class="language-plaintext highlighter-rouge">Hadoop</code>集群上执行。
<img src="../../../../resource/2022/hive/hive_architecture.jpg" width="800" alt="Hive整体设计" /></li>
</ol>
<h3 id="hive数据模型"><code class="language-plaintext highlighter-rouge">Hive</code>数据模型</h3>
<p><code class="language-plaintext highlighter-rouge">Hive</code>通过以下模型来组织<code class="language-plaintext highlighter-rouge">HDFS</code>上的数据,包括:数据库<code class="language-plaintext highlighter-rouge">DataBase</code>、表<code class="language-plaintext highlighter-rouge">Table</code>、分区<code class="language-plaintext highlighter-rouge">Partition</code>和桶<code class="language-plaintext highlighter-rouge">Bucket</code>。</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">Table</code>管理表和外表,<code class="language-plaintext highlighter-rouge">Hive</code>中的表和关系数据库中的表很类似,依据数据是否受<code class="language-plaintext highlighter-rouge">Hive</code>管理可分为:<code class="language-plaintext highlighter-rouge">Managed Table</code>(内表)和<code class="language-plaintext highlighter-rouge">External Table</code>(外表)。对于内表,<code class="language-plaintext highlighter-rouge">HDFS</code>上存储的数据由<code class="language-plaintext highlighter-rouge">Hive</code>管理,<code class="language-plaintext highlighter-rouge">Hive</code>对表的删除影响实际的数据。外表则只是一个数据的映射,<code class="language-plaintext highlighter-rouge">Hive</code>对表的删除仅仅删除愿数据,实际数据不受影响。</li>
<li><code class="language-plaintext highlighter-rouge">Partition</code>基于用户指定的列的值对数据表进行分区,每一个分区对应表下的相应目录<code class="language-plaintext highlighter-rouge">${hive.metastore.warehouse.dir}/{database_name}.db/{tablename}/{partition key}={value}</code>,其优点在于从物理上分目录划分不同列的数据,易于查询的简枝,提升查询的效率。</li>
<li><code class="language-plaintext highlighter-rouge">Bucket</code>桶作为另一种数据组织方式,弥补<code class="language-plaintext highlighter-rouge">Partition</code>的短板,通过<code class="language-plaintext highlighter-rouge">Bucket</code>列的值进行<code class="language-plaintext highlighter-rouge">Hash</code>散列到相应的文件中,有利于查询优化、对于抽样非常有效。</li>
</ol>
<h3 id="hive的数据存储格式聊聊parquet"><code class="language-plaintext highlighter-rouge">Hive</code>的数据存储格式,聊聊<code class="language-plaintext highlighter-rouge">Parquet*</code></h3>
<p><code class="language-plaintext highlighter-rouge">Parquet*</code>起源于<code class="language-plaintext highlighter-rouge">Google Dremel</code>系统,相当于<code class="language-plaintext highlighter-rouge">Dremel</code>中的数据存储引擎。最初的设计动机是存储嵌套式数据,如<code class="language-plaintext highlighter-rouge">Protocolbuffer</code>、<code class="language-plaintext highlighter-rouge">thrift</code>和<code class="language-plaintext highlighter-rouge">json</code>等,将这些数据存储成列式格式,以便于对其高效压缩和编码,且使用更少的<code class="language-plaintext highlighter-rouge">IO</code>操作取出需要的数据。并且其存储<code class="language-plaintext highlighter-rouge">metadata</code>,支持<code class="language-plaintext highlighter-rouge">schema</code>的变更。
<img src="../../../../resource/2022/hive/parquent-format.jpg" width="760" alt="Parquet格式" />
<code class="language-plaintext highlighter-rouge">Parquet*</code>是面向分析型业务的列式存储格式,由<code class="language-plaintext highlighter-rouge">Twitter</code>和<code class="language-plaintext highlighter-rouge">Cloudera</code>合作开发。一个<code class="language-plaintext highlighter-rouge">Parquet</code>文件通常由一个<code class="language-plaintext highlighter-rouge">header</code>和一个或多个<code class="language-plaintext highlighter-rouge">block</code>块组成,以一个<code class="language-plaintext highlighter-rouge">footer</code>结尾。<code class="language-plaintext highlighter-rouge">footer</code>中的<code class="language-plaintext highlighter-rouge">metadata</code>包含了格式的版本信息、<code class="language-plaintext highlighter-rouge">schema</code>信息、<code class="language-plaintext highlighter-rouge">key-value pairs</code>以及所有<code class="language-plaintext highlighter-rouge">block</code>中的<code class="language-plaintext highlighter-rouge">metadata</code>信息。</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">parquent-format</code>项目定义了<code class="language-plaintext highlighter-rouge">parquent</code>内部的数据类型、存储格式等。</li>
<li><code class="language-plaintext highlighter-rouge">parquent-mr</code>项目完成外部对象模型与<code class="language-plaintext highlighter-rouge">parquent</code>内部数据类型的映射,对象模型可以简单理解为内存中的数据表示,<code class="language-plaintext highlighter-rouge">Avro</code>、<code class="language-plaintext highlighter-rouge">Thrift、Protocol Buffers</code>等这些都是对象模型。</li>
</ul>
<h3 id="hive-catalog介绍"><code class="language-plaintext highlighter-rouge">Hive Catalog</code>介绍</h3>
<ul>
<li><code class="language-plaintext highlighter-rouge">HCatalog</code>是<code class="language-plaintext highlighter-rouge">Hadoop</code>的元数据和数据表的管理系统,它基于<code class="language-plaintext highlighter-rouge">Hive</code>中的元数据层,通过类似<code class="language-plaintext highlighter-rouge">SQL</code>的语言展现<code class="language-plaintext highlighter-rouge">Hadoop</code>数据的关联关系。</li>
<li><code class="language-plaintext highlighter-rouge">Catalog</code>允许用户通过<code class="language-plaintext highlighter-rouge">Hive</code>、<code class="language-plaintext highlighter-rouge">Pig</code>、<code class="language-plaintext highlighter-rouge">MapReduce</code>共享数据和元数据,用户编写应用程序时,无需关心数据怎样存储、在哪里存储,避免因<code class="language-plaintext highlighter-rouge">schema</code>和存储格式的改变而受到影响。</li>
<li>通过<code class="language-plaintext highlighter-rouge">HCatalog</code>,用户能通过工具访问<code class="language-plaintext highlighter-rouge">Hadoop</code>上的<code class="language-plaintext highlighter-rouge">Hive Metastore</code>。它为<code class="language-plaintext highlighter-rouge">MapReduce</code>和<code class="language-plaintext highlighter-rouge">Pig</code>提供了连接器,用户可以使用工具对<code class="language-plaintext highlighter-rouge">Hive</code>的关联列格式的数据进行读写。
<img src="../../../../resource/2022/hive/hive-catalog.jpg" width="820" alt="HCatalog元数据管理" /></li>
</ul>
<h3 id="数据类型及数据定义">数据类型及数据定义</h3>
<p>hive支持基本数据类型有<code class="language-plaintext highlighter-rouge">tinyInt</code>、<code class="language-plaintext highlighter-rouge">int</code>、<code class="language-plaintext highlighter-rouge">bigInt</code>、<code class="language-plaintext highlighter-rouge">String</code>等,除此之外,其还支持复杂类型,如<code class="language-plaintext highlighter-rouge">struct</code>、<code class="language-plaintext highlighter-rouge">map</code>和<code class="language-plaintext highlighter-rouge">array</code>等。Hive中默认以<code class="language-plaintext highlighter-rouge">\n</code>作为行分割符,以<code class="language-plaintext highlighter-rouge">^A</code>用于字段分割符,用<code class="language-plaintext highlighter-rouge">^B</code>分割array或struct中的元素,或用于map中键-值对之间的分割,使用<code class="language-plaintext highlighter-rouge">^C</code>用于map中键和值之间的分割。</p>
<p>若要实现自定义话,需用一组<code class="language-plaintext highlighter-rouge">row format delimited</code>语句,分别指定行、字段、map、list的分割符:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">create</span> <span class="k">table</span> <span class="n">employees</span><span class="p">(</span>
<span class="p">...</span><span class="n">field</span> <span class="n">list</span>
<span class="p">)</span> <span class="k">row</span> <span class="n">format</span> <span class="n">delimited</span>
<span class="n">fields</span> <span class="n">terminated</span> <span class="k">by</span> <span class="nv">`</span><span class="se">\0</span><span class="nv">01`</span>
<span class="n">collection</span> <span class="n">items</span> <span class="n">terminated</span> <span class="k">by</span> <span class="nv">`</span><span class="se">\0</span><span class="nv">02`</span>
<span class="k">map</span> <span class="n">keys</span> <span class="n">terminated</span> <span class="k">by</span> <span class="nv">`</span><span class="se">\0</span><span class="nv">03`</span>
<span class="n">lines</span> <span class="n">terminated</span> <span class="k">by</span> <span class="nv">`</span><span class="se">\n</span><span class="nv">`</span> <span class="n">stored</span> <span class="k">as</span> <span class="n">textfile</span><span class="p">;</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">hive</code>数据表分为管理表和外部表,<code class="language-plaintext highlighter-rouge">external</code>表用于加载外部数据源,删除外部表并不会删除<code class="language-plaintext highlighter-rouge">hdfs</code>上的文件数据,有些<code class="language-plaintext highlighter-rouge">HiveQL</code>语法结构并不适用于外部表。<code class="language-plaintext highlighter-rouge">hive</code>中有数据分区的概念,可以看到分区表具有重要的性优势,而且分区表还可以用一种符合逻辑分方式进行组织,比如分层存储。</p>
<p>创建好表之后,可用<code class="language-plaintext highlighter-rouge">hsql</code>从<code class="language-plaintext highlighter-rouge">hdfs</code>中向hive表加载数据,用<code class="language-plaintext highlighter-rouge">overwrite</code>会完全覆盖表中的记录:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">LOAD</span> <span class="k">DATA</span> <span class="n">INPATH</span> <span class="s1">'/tmp/hive/metastore/financials.db/employees/employee-22-0927.csv'</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="n">employees</span><span class="p">;</span>
</code></pre></div></div>
<h3 id="udf和自定义fileformat">UDF和自定义FileFormat</h3>
<p>在<code class="language-plaintext highlighter-rouge">hive</code>中用户可以自定义实现<code class="language-plaintext highlighter-rouge">UDF</code>,对hive库已有的函数进行扩展,例子,自定义<code class="language-plaintext highlighter-rouge">UDF</code>实现计算每个人所属的星座功能。实现类<code class="language-plaintext highlighter-rouge">UDFZodiacSign</code>继承基类<code class="language-plaintext highlighter-rouge">UDF</code>并实现<code class="language-plaintext highlighter-rouge">evaluate()</code>函数,在查询中对于每行输入都会应用到<code class="language-plaintext highlighter-rouge">evaluate()</code>函数,而<code class="language-plaintext highlighter-rouge">evaluate()</code>处理后的值会返回给<code class="language-plaintext highlighter-rouge">Hive</code>。</p>
<p>加载<code class="language-plaintext highlighter-rouge">hadoop-mapreduce-1.0.0.xx.jar</code>到<code class="language-plaintext highlighter-rouge">hive</code>中,只与当前<code class="language-plaintext highlighter-rouge">session</code>会话进行了绑定。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hive> add jar /Users/madong/datahub-repository/distributed-data-computing/hadoop-mapreduce/target/hadoop-mapreduce-1.0.0-jar-with-dependencies.jar
</code></pre></div></div>
<p>将函数<code class="language-plaintext highlighter-rouge">zodiac</code>注册到<code class="language-plaintext highlighter-rouge">hive</code>中,可以用<code class="language-plaintext highlighter-rouge">describe function extended zodiac</code>来查看函数明细内容:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hive> create temporary <span class="k">function </span>zodiac as <span class="s1">'hadoop.apache.hive.UDFZodiacSign'</span><span class="p">;</span>
<span class="c"># 实际执行真正的sql,zodiac(date)将日期转为了对应的星座</span>
hive <span class="o">(</span>financials<span class="o">)></span> <span class="k">select </span>name, zodiac<span class="o">(</span>bday<span class="o">)</span> from littlebigdata<span class="p">;</span>
OK
name _c1
edward capriolo Aquarius
</code></pre></div></div>
<p>在使用完<code class="language-plaintext highlighter-rouge">UDF</code>后,可以通过<code class="language-plaintext highlighter-rouge">drop temporary function if exists zodiac</code>删除此函数。<code class="language-plaintext highlighter-rouge">UDAF</code>自定义扩展和<code class="language-plaintext highlighter-rouge">UDF</code>一样,但其继承的是<code class="language-plaintext highlighter-rouge">GenericUDF</code>类,要想使所有函数都长期有效,可在<code class="language-plaintext highlighter-rouge">FunctionRegistry</code>中注册,然后重新替换<code class="language-plaintext highlighter-rouge">hive-exec-*.jar</code>这个jar文件就可以。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">registrUDF</span><span class="o">(</span><span class="err">'</span><span class="n">parse_url</span><span class="err">'</span><span class="o">,</span> <span class="nc">UDFParseUrl</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="kc">false</span><span class="o">)</span>
<span class="n">registerGenericUDF</span><span class="o">(</span><span class="err">'</span><span class="n">nvl</span><span class="err">'</span><span class="o">,</span> <span class="nc">GenericUDFNvl</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="n">registerGenericUDF</span><span class="o">(</span><span class="err">'</span><span class="n">split</span><span class="err">'</span><span class="o">,</span> <span class="nc">GenericUDFSplit</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">FileFormat</code>是用自定义的方式从<code class="language-plaintext highlighter-rouge">HDFS</code>上读取内容,按指定的格式切分<code class="language-plaintext highlighter-rouge">fields</code>以及<code class="language-plaintext highlighter-rouge">row</code>数据,实现方式可参考<code class="language-plaintext highlighter-rouge">Base64TextInputFormat</code>和<code class="language-plaintext highlighter-rouge">Base64TextOutputFormat</code>。</p>
<ul>
<li>hive conflient: https://cwiki.apache.org/confluence/display/Hive/DeveloperGuide#DeveloperGuide-RegistrationofNativeSerDes</li>
<li>base64 fileformat: https://github.com/apache/hive/tree/master/contrib/src/java/org/apache/hadoop/hive/contrib/fileformat</li>
</ul>
大数据三架马车之Yarn、BigTable
2021-11-07T00:00:00+00:00
https://dongma.github.io/2021/11/07/yarn-resource-scheduler
<p>在<code class="language-plaintext highlighter-rouge">Hadoop V1.0</code>版本中,资源调度部分存在扩展性差、可用性差、资源利用率低的改问题,其中,<code class="language-plaintext highlighter-rouge">Job Tracker</code>既要做资源管理,又要做任务监控,同时<code class="language-plaintext highlighter-rouge">Job</code>的并发数页存在限制。同时,<code class="language-plaintext highlighter-rouge">JobTracker</code>存在单点故障问题,任务调度部分不支持调度流式计算、迭代计算、DAG模型。</p>
<p><code class="language-plaintext highlighter-rouge">2013</code>年,<code class="language-plaintext highlighter-rouge">Hadoop 2.0</code>发布,引入了<code class="language-plaintext highlighter-rouge">Yarn</code>、<code class="language-plaintext highlighter-rouge">HDFS HA</code>、<code class="language-plaintext highlighter-rouge">Federation</code>。</p>
<h3 id="yarn的设计思路yet-another-resource-manager">Yarn的设计思路(Yet Another Resource Manager)</h3>
<p><code class="language-plaintext highlighter-rouge">Yarn</code>由三部分组成:<code class="language-plaintext highlighter-rouge">ResourceManager</code>、<code class="language-plaintext highlighter-rouge">NodeManager</code>、<code class="language-plaintext highlighter-rouge">ApplicationMaster</code>,其中:<code class="language-plaintext highlighter-rouge">RM</code>掌控全局的资源,负责整个系统的资源管理和分配(处理客户端请求、启动/监控<code class="language-plaintext highlighter-rouge">AM</code>和<code class="language-plaintext highlighter-rouge">NM</code>、资源调度和分配),<code class="language-plaintext highlighter-rouge">NM</code>驻留在一个<code class="language-plaintext highlighter-rouge">YARN</code>集群的节点上做代理,管理单个节点的资源、处理<code class="language-plaintext highlighter-rouge">RM</code>、<code class="language-plaintext highlighter-rouge">AM</code>的命令,<code class="language-plaintext highlighter-rouge">AM</code>为应用程序管理器,负责系统中所有所有应用程序的管理工作(数据切分、为<code class="language-plaintext highlighter-rouge">APP</code>申请资源并分配、任务监控和容错)。</p>
<p><code class="language-plaintext highlighter-rouge">Yarn</code>主要解决数据集群资源利用率低、数据无法共享、维护成本高的问题,常见的应用场景有:<code class="language-plaintext highlighter-rouge">MapReduce</code>实现离线批处理、<code class="language-plaintext highlighter-rouge">Impala</code>实现交互式查询分析、用<code class="language-plaintext highlighter-rouge">Strom</code>实现流式计算、在<code class="language-plaintext highlighter-rouge">Spark</code>下来完成迭代计算。
<!-- more --></p>
<h3 id="yarn-container及资源调度流程">Yarn Container及资源调度流程</h3>
<p><code class="language-plaintext highlighter-rouge">Container</code>是<code class="language-plaintext highlighter-rouge">Yarn</code>资源的抽象,它封装了某个节点上一定量的资源(<code class="language-plaintext highlighter-rouge">CPU</code>和内存两类资源),它跟<code class="language-plaintext highlighter-rouge">Linux Container</code>没有任何关系,仅仅是<code class="language-plaintext highlighter-rouge">Yarn</code>提出的一个概念(可序列、反序列的对象)。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">message</span> <span class="nc">ContainerProto</span> <span class="o">{</span>
<span class="n">optional</span> <span class="nc">ContainerIdProto</span> <span class="n">id</span> <span class="k">=</span> <span class="mi">1</span><span class="o">;</span> <span class="c1">// container id</span>
<span class="n">optional</span> <span class="nc">NodeIdProto</span> <span class="n">nodeId</span> <span class="k">=</span> <span class="mi">2</span><span class="o">;</span> <span class="c1">// 资源所在节点</span>
<span class="n">optional</span> <span class="n">string</span> <span class="n">node_http_address</span> <span class="k">=</span> <span class="mi">3</span><span class="o">;</span>
<span class="n">optional</span> <span class="nc">ResourceProto</span> <span class="n">resource</span> <span class="k">=</span> <span class="mi">4</span><span class="o">;</span> <span class="c1">// container资源量</span>
<span class="n">optional</span> <span class="nc">PriorityProto</span> <span class="n">priority</span> <span class="k">=</span> <span class="mi">5</span><span class="o">;</span> <span class="c1">// container优先级</span>
<span class="n">optional</span> <span class="nv">hadoop</span><span class="o">.</span><span class="py">common</span><span class="o">.</span><span class="py">TokenProto</span> <span class="n">container_token</span> <span class="k">=</span> <span class="mi">6</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Container</code>由<code class="language-plaintext highlighter-rouge">ApplicationMaster</code>向<code class="language-plaintext highlighter-rouge">ResourceManager</code>申请的,由<code class="language-plaintext highlighter-rouge">ResourceManager</code>中的资源调度器异步分配给<code class="language-plaintext highlighter-rouge">ApplicationMaster</code>。<code class="language-plaintext highlighter-rouge">Container</code>的运行是由<code class="language-plaintext highlighter-rouge">ApplicationMaster</code>向资源所在的<code class="language-plaintext highlighter-rouge">NodeManager</code>发起的,<code class="language-plaintext highlighter-rouge">Container</code>运行时需提供内部执行的任何命令(比如<code class="language-plaintext highlighter-rouge">Java</code>、<code class="language-plaintext highlighter-rouge">Python</code>、<code class="language-plaintext highlighter-rouge">C++</code>进程启动命令均可)及该命令执行所需的环境变量和外部资源。</p>
<h3 id="资源调度算法及调度器">资源调度算法及调度器</h3>
<p>调度算法是整个资源管理系统中一个重要的部分,简单地说,调度算法的作用是决定一个计算任务需要放在集群中的哪台机器上面。待调度的任务需考虑资源需求(<code class="language-plaintext highlighter-rouge">CPU</code>、<code class="language-plaintext highlighter-rouge">Memory</code>、<code class="language-plaintext highlighter-rouge">Disk</code>),应用亲和及反亲和性等。</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">FIFO</code>调度,先来的先被调用、分配<code class="language-plaintext highlighter-rouge">CPU</code>、内存等资源,后来的在队列等待。适用于平均计算时间、耗时资源差不多的作业,通常还可匹配优先级,不足在于用户将<code class="language-plaintext highlighter-rouge">Job</code>作业优先级设置的最高时,会导致排在后面的短任务等待。</li>
<li><code class="language-plaintext highlighter-rouge">SJF(Shortest Job First)</code>调度,为了改善<code class="language-plaintext highlighter-rouge">FIFO</code>算法,减少平均周转时间,提出了短作业优先算法。任务执行前预先计算好其执行时间,调度器从中选择用时较短的任务优先执行,但优先级无法保证。</li>
<li>时间片轮转调度<code class="language-plaintext highlighter-rouge">(Round Robin,RR)</code>,核心思想是<code class="language-plaintext highlighter-rouge">CPU</code>时间分片(<code class="language-plaintext highlighter-rouge">time slice</code>)轮转就绪任务,当时间片结束时,任务未执行完时发生时钟中断,调度器会暂停当前任务的执行,并将其置于就绪队列的末尾。此调度优点在于跟任务大小无关,都可获得公平的资源分配。但实现较为复杂,计算框架需支持中断。</li>
<li>最大最小公平调度(<code class="language-plaintext highlighter-rouge">Min-Max Fair</code>),将资源平分为<code class="language-plaintext highlighter-rouge">n</code>份(每份<code class="language-plaintext highlighter-rouge">S/n</code>),把每份分给相应的用户。若超过了用户的需求,就回收超过的部分,然后将总体回收的资源平均分给上一轮分配中尚未得到满足的用户,直到没有回收的资源为止。</li>
<li>容量调度(<code class="language-plaintext highlighter-rouge">Capacity</code>),首先划分多个队列,队列资源采用容量占比的方式进行分配。每个队列设置资源最低保证和使用上限。如果队列中的资源有剩余或空闲,可暂时共享给那些需要资源的队列,一旦该队列有新的应用程序需要运行资源,则其它队列释放的资源会归还给该队列。</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">Yarn</code>的三种调度器实现为:<code class="language-plaintext highlighter-rouge">Fair Scheduler</code>(公平调度器)、<code class="language-plaintext highlighter-rouge">FIFO Scheduler</code>(先进先出调度器)、<code class="language-plaintext highlighter-rouge">Fair Scheduler</code>(公平调度器),<code class="language-plaintext highlighter-rouge">FIFO</code>先进先出调度器,同一时间队列中只有一个任务在执行,可以充分利用所有的集群资源。<code class="language-plaintext highlighter-rouge">Fair Scheduler</code>和<code class="language-plaintext highlighter-rouge">Capacity Scheduler</code>有区别的一些地方,<code class="language-plaintext highlighter-rouge">Fair</code>队列内部支持多种调度策略,包括<code class="language-plaintext highlighter-rouge">FIFO</code>、<code class="language-plaintext highlighter-rouge">Fair</code>、<code class="language-plaintext highlighter-rouge">DRF(Dominant Resource Fairness)</code>多种资源类型(<code class="language-plaintext highlighter-rouge">e.g.CPU</code>、内存的公平资源分配策略)。</p>
<h3 id="job提交流程">Job提交流程</h3>
<p>在<code class="language-plaintext highlighter-rouge">yarn</code>上提交<code class="language-plaintext highlighter-rouge">job</code>的流程如下方的步骤图所示,<code class="language-plaintext highlighter-rouge">yarnRunner</code>向<code class="language-plaintext highlighter-rouge">rm</code>申请一个<code class="language-plaintext highlighter-rouge">Application</code>,<code class="language-plaintext highlighter-rouge">rm</code>返回一个资源提交路径和<code class="language-plaintext highlighter-rouge">application_id</code>,客户端提交<code class="language-plaintext highlighter-rouge">job</code>所需要的资源(切片+配置信息+<code class="language-plaintext highlighter-rouge">jar</code>包)到资源提交路径。
<img src="../../../../resource/2021/yarn/yarn-submit-job.jpg" width="650" alt="yarn上job提交流程" />
<code class="language-plaintext highlighter-rouge">Capacity Scheduler</code>参数调整是在<code class="language-plaintext highlighter-rouge">yarn-site.xml</code>中,<code class="language-plaintext highlighter-rouge">yarn.resourcemanager.scheduler.class</code>用于配置调度策略<code class="language-plaintext highlighter-rouge">org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</code>。</p>
<p><code class="language-plaintext highlighter-rouge">Yarn</code>的高级特性<code class="language-plaintext highlighter-rouge">Node Label</code>,<code class="language-plaintext highlighter-rouge">HDFS</code>异构存储只能设置让某些数据(以目录为单位)分别在不同的存储介质上,但是计算调度时无法保障作业运行的环境。在<code class="language-plaintext highlighter-rouge">Nodel Label</code>出现之前,资源申请方无法指定资源类型、软件运行的环境(<code class="language-plaintext highlighter-rouge">JDK</code>、<code class="language-plaintext highlighter-rouge">python</code>)等,目前只有<code class="language-plaintext highlighter-rouge">Capacity Scheduler</code>支持此功能,<code class="language-plaintext highlighter-rouge">Fair Scheduler</code>正在开发,<code class="language-plaintext highlighter-rouge">yarn.node-labels.enable</code>用于开启<code class="language-plaintext highlighter-rouge">Node Label</code>的配置。</p>
<h3 id="bigtable的开源实现hbase">BigTable的开源实现HBase</h3>
<p><code class="language-plaintext highlighter-rouge">BigTable</code>是一个分布式存储系统,用于管理结构化数据,旨在扩展到非常大的规模:数千个商品服务器上的<code class="language-plaintext highlighter-rouge">PB</code>级数据。<code class="language-plaintext highlighter-rouge">Google</code>的很多项目使用<code class="language-plaintext highlighter-rouge">HBase</code>来存储数据,包括:网页索引、<code class="language-plaintext highlighter-rouge">google</code>地图和<code class="language-plaintext highlighter-rouge">google</code>金融,这些应用程序在数据大小(从<code class="language-plaintext highlighter-rouge">URL</code>到网页再到卫星图像)和延迟要求(从后端批量处理到实时数据服务)方面对<code class="language-plaintext highlighter-rouge">BigTable</code>提出了不同的要求。</p>
<p><code class="language-plaintext highlighter-rouge">Hbase</code>是一个稀疏的、分布式的、持久的多纬排序图,该映射由行键、列键和时间戳索引组成,<code class="language-plaintext highlighter-rouge">map</code>中的每个值都是一个不可序列化的字节数组。其对应的数据模型(逻辑视图)如下:</p>
<p><img src="../../../../resource/2021/hbase/big_table_data_model.jpg" width="600" alt="HBase数据模型" /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(row:string, column:string, time:int64) -> string
</code></pre></div></div>
<ul>
<li>行关键字<code class="language-plaintext highlighter-rouge">Row key</code>,唯一标识一行数据,用于检索记录。其可以是任意长度的字符串,最大长度为<code class="language-plaintext highlighter-rouge">64</code>KB。存储时会按照<code class="language-plaintext highlighter-rouge">row key</code>的字典序进行排序,其可通过单个<code class="language-plaintext highlighter-rouge">row key</code>、<code class="language-plaintext highlighter-rouge">row key</code>的<code class="language-plaintext highlighter-rouge">range</code>及全表扫描的方式来访问。</li>
<li><code class="language-plaintext highlighter-rouge">Column Family</code>,行中的列被成为列族,同一个列族的所有成员具有相同的列族前缀,列键<code class="language-plaintext highlighter-rouge">Column Key</code>也称为列名,必须以列族作为前缀,格式为列族:限定词。</li>
<li><code class="language-plaintext highlighter-rouge">Timestamp</code>和<code class="language-plaintext highlighter-rouge">Cell</code>,插入单元格时的时间戳,默认作为单元格的版本号,类型为<code class="language-plaintext highlighter-rouge">64</code>位整数。要定位一个单元,需满足”行键+列键+时间戳”三个要素。</li>
</ul>
<p>在物理视图上,<code class="language-plaintext highlighter-rouge">HBase</code>的每一个列族<code class="language-plaintext highlighter-rouge">Column Family</code>对应一个<code class="language-plaintext highlighter-rouge">StoredFile</code>对象,服务组件整体分为<code class="language-plaintext highlighter-rouge">HMaster</code>和<code class="language-plaintext highlighter-rouge">Region Server</code>两部分,底层使用<code class="language-plaintext highlighter-rouge">Hadoop</code>的<code class="language-plaintext highlighter-rouge">DataNode</code>来存储数据。</p>
大数据三架马车之MapReduce
2021-08-09T00:00:00+00:00
https://dongma.github.io/2021/08/09/MapReduce-Programming-Model
<blockquote>
<p><code class="language-plaintext highlighter-rouge">Hadoop</code>是<code class="language-plaintext highlighter-rouge">Apache</code>的一个开源的分布式计算平台,以<code class="language-plaintext highlighter-rouge">HDFS</code>分布式文件系统和<code class="language-plaintext highlighter-rouge">MapReduce</code>计算框架为核心,为用户提供一套底层透明的分布式基础设施。</p>
</blockquote>
<p><code class="language-plaintext highlighter-rouge">MapReduce</code>提供简单的<code class="language-plaintext highlighter-rouge">API</code>,允许用户在不了解底层细节的情况下,开发分布式并行程序。利用大规模集群资源,解决传统单机无法解决的大数据处理问题,其设计思想起源于<code class="language-plaintext highlighter-rouge">MapReduce Paper</code>。</p>
<h3 id="mapreduce编程模型">MapReduce编程模型</h3>
<p><code class="language-plaintext highlighter-rouge">MapReduce</code>是一种用于处理和生成大型数据集的编程模型和相关实现,用户指定一个<code class="language-plaintext highlighter-rouge">map()</code>函数接收处理<code class="language-plaintext highlighter-rouge">key/value</code>对,同时产生另外一组临时<code class="language-plaintext highlighter-rouge">key/value</code>集合,<code class="language-plaintext highlighter-rouge">reduce()</code>函数合并相同<code class="language-plaintext highlighter-rouge">intermediate key</code>关联的<code class="language-plaintext highlighter-rouge">value</code>数据,以这种函数式方风格写的程序会自动并行化并在大型商品机器集群上运行。</p>
<p>在<code class="language-plaintext highlighter-rouge">Paper</code>发布之前的几年,<code class="language-plaintext highlighter-rouge">Jeffrey Dean</code>及<code class="language-plaintext highlighter-rouge">Google</code>的一些工程师已经实现了数百个用于处理大量原始数据且特殊用途的计算程序,数据源如抓取的文档、<code class="language-plaintext highlighter-rouge">Web</code>日志的请求等,来计算各种派生数据,像倒排索引、<code class="language-plaintext highlighter-rouge">Web</code>文档图结构的各种表示、每个主机爬取的页面数汇总等。
<!-- more --></p>
<p>看个统计单词在文章中出现的次数的例子,<code class="language-plaintext highlighter-rouge">map()</code>函数<code class="language-plaintext highlighter-rouge">emit</code>每个单词及其出现次数,<code class="language-plaintext highlighter-rouge">reduce()</code>函数统计按单词统计其出现的总次数,伪代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">map</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">,</span> <span class="nc">String</span> <span class="n">value</span><span class="o">):</span>
<span class="c1">// key: document name</span>
<span class="c1">// value: document contents for each word w in value:</span>
<span class="nc">EmitIntermediate</span><span class="o">(</span><span class="n">w</span><span class="o">,</span> <span class="s">"1"</span><span class="o">);</span>
<span class="n">reduce</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">,</span> <span class="nc">Iterator</span> <span class="n">values</span><span class="o">):</span> <span class="c1">// key: a word</span>
<span class="c1">// values: a list of counts</span>
<span class="kt">int</span> <span class="n">result</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="k">for</span> <span class="n">each</span> <span class="n">v</span> <span class="n">in</span> <span class="nl">values:</span> <span class="n">result</span> <span class="o">+=</span> <span class="nc">ParseInt</span><span class="o">(</span><span class="n">v</span><span class="o">);</span>
<span class="nc">Emit</span><span class="o">(</span><span class="nc">AsString</span><span class="o">(</span><span class="n">result</span><span class="o">));</span>
</code></pre></div></div>
<p>谈一些<code class="language-plaintext highlighter-rouge">MapReduce</code>程序在<code class="language-plaintext highlighter-rouge">Google</code>的应用:</p>
<ul>
<li>反向索引(<code class="language-plaintext highlighter-rouge">Inverted Index</code>),<code class="language-plaintext highlighter-rouge">map()</code>函数解析文档中的每个单词,对<code class="language-plaintext highlighter-rouge">(word, document ID)</code>进行<code class="language-plaintext highlighter-rouge">emit</code>,<code class="language-plaintext highlighter-rouge">reduce</code>端依据<code class="language-plaintext highlighter-rouge">word</code>将二元组<code class="language-plaintext highlighter-rouge">pair</code>中的<code class="language-plaintext highlighter-rouge">document id</code>进行合并,最终<code class="language-plaintext highlighter-rouge">emit(word, list(document ID))</code>的数据。</li>
<li>统计<code class="language-plaintext highlighter-rouge">URL</code>的访问频次,<code class="language-plaintext highlighter-rouge">map()</code>函数输出从<code class="language-plaintext highlighter-rouge">log</code>中获取<code class="language-plaintext highlighter-rouge">web request</code>的信息,将<code class="language-plaintext highlighter-rouge">(URL, 1)</code>的二元组进行<code class="language-plaintext highlighter-rouge">output</code>,<code class="language-plaintext highlighter-rouge">reduce()</code>端将具有相同<code class="language-plaintext highlighter-rouge">URL</code>的请求进行归集,以<code class="language-plaintext highlighter-rouge">(URL, total count)</code>的方式进行<code class="language-plaintext highlighter-rouge">emit</code>。</li>
<li>反转<code class="language-plaintext highlighter-rouge">Web-Link Graph</code>,<code class="language-plaintext highlighter-rouge">map()</code>函数针对网页中存在的超链接,按<code class="language-plaintext highlighter-rouge">(target, source)</code>的格式输出二元组,<code class="language-plaintext highlighter-rouge">reduce()</code>函数将相同<code class="language-plaintext highlighter-rouge">target</code>对应的<code class="language-plaintext highlighter-rouge">source</code>拼接起来,按<code class="language-plaintext highlighter-rouge">(target, list(source))</code>的方式进行<code class="language-plaintext highlighter-rouge">emit</code>。</li>
</ul>
<h3 id="execution-overview">Execution Overview</h3>
<p>在输入数据切分成数份后,<code class="language-plaintext highlighter-rouge">map</code>函数会被自动分发到多台机器上,输入数据切分可以在多不同的机器上并行执行。<code class="language-plaintext highlighter-rouge">partition()</code>函数(<code class="language-plaintext highlighter-rouge">hash(key) mod R</code>)会将中间<code class="language-plaintext highlighter-rouge">key</code>切分为<code class="language-plaintext highlighter-rouge">R</code>片,<code class="language-plaintext highlighter-rouge">reduce()</code>函数会根据切分结果分到不同机器。</p>
<p><img src="../../../../resource/2021/mapreduce/mapreduce_execution_overview.png" width="800" alt="mapreduce执行概览" /></p>
<ol>
<li>用户程序中的<code class="language-plaintext highlighter-rouge">mapreduce library</code>会将输入的文件切分成<code class="language-plaintext highlighter-rouge">16MB</code>~<code class="language-plaintext highlighter-rouge">64MB</code>的文件块,然后它在<code class="language-plaintext highlighter-rouge">cluster</code>中启动多个副本。</li>
<li>在<code class="language-plaintext highlighter-rouge">cluster</code>上跑的多个程序中有一个是特殊的,其为<code class="language-plaintext highlighter-rouge">master</code>节点,剩余的为<code class="language-plaintext highlighter-rouge">worker</code>节点。<code class="language-plaintext highlighter-rouge">master</code>节点向<code class="language-plaintext highlighter-rouge">worker</code>节点分配任务,当<code class="language-plaintext highlighter-rouge">worker</code>节点有空闲时,会向其分配<code class="language-plaintext highlighter-rouge">map</code>或<code class="language-plaintext highlighter-rouge">reduce</code>任务。</li>
<li><code class="language-plaintext highlighter-rouge">worker</code>执行<code class="language-plaintext highlighter-rouge">map</code>任务时,会从切分的文件中读取数据,它从文件中读取<code class="language-plaintext highlighter-rouge">key/value</code>对,在<code class="language-plaintext highlighter-rouge">map()</code>函数中执行数据处理,生成的<code class="language-plaintext highlighter-rouge">intermediate key</code>会缓存在<code class="language-plaintext highlighter-rouge">memory</code>中。</li>
<li><code class="language-plaintext highlighter-rouge">memory</code>中的<code class="language-plaintext highlighter-rouge">pair</code>会定期的写入本地磁盘,并将其位置信息返给<code class="language-plaintext highlighter-rouge">master</code>节点,其负责将<code class="language-plaintext highlighter-rouge">buffered pair</code>对应的位置信息转发给其它<code class="language-plaintext highlighter-rouge">worker</code>节点。</li>
<li>当<code class="language-plaintext highlighter-rouge">master</code>节点将<code class="language-plaintext highlighter-rouge">map</code>函数产生的中间数据位置告知<code class="language-plaintext highlighter-rouge">reduce worker</code>时,其会使用<code class="language-plaintext highlighter-rouge">rpc</code>从<code class="language-plaintext highlighter-rouge">map worker</code>的本地磁盘中读取数据。当<code class="language-plaintext highlighter-rouge">reduce worker</code>读完所有数据后,它会对<code class="language-plaintext highlighter-rouge">intermediate key</code>对数据进行排序,因此,具有相同<code class="language-plaintext highlighter-rouge">key</code>的中间结果就会被<code class="language-plaintext highlighter-rouge">group</code>在一起。<code class="language-plaintext highlighter-rouge">sort</code>是非常必要的,因为通常情况下,许多不同的<code class="language-plaintext highlighter-rouge">key</code>会映射到相同的<code class="language-plaintext highlighter-rouge">reduce</code>函数中。当中间数据太大在<code class="language-plaintext highlighter-rouge">memory</code>中放不下时,会使用外部排序进行处理。</li>
<li><code class="language-plaintext highlighter-rouge">reduce worker</code>会遍历排序后的中间数据,将<code class="language-plaintext highlighter-rouge">intermediate key</code>及对应<code class="language-plaintext highlighter-rouge">value</code>集合传给<code class="language-plaintext highlighter-rouge">reduce</code>函数,<code class="language-plaintext highlighter-rouge">reduce()</code>函数的输出结果会<code class="language-plaintext highlighter-rouge">append</code>到一个最终的输出文件中。当所有<code class="language-plaintext highlighter-rouge">map</code>和<code class="language-plaintext highlighter-rouge">reduce</code>任务都执行完成后,它会告知用户程序返回用户代码。</li>
</ol>
<h3 id="容错性考虑">容错性考虑</h3>
<p>由于<code class="language-plaintext highlighter-rouge">mapreduce</code>旨在帮助使用成百上千台机器处理处理大量数据,因此该机器必须优雅地容忍机器故障,分别讨论下当<code class="language-plaintext highlighter-rouge">worker</code>和<code class="language-plaintext highlighter-rouge">master</code>节点故障时,如何进行容错?</p>
<p><strong><code class="language-plaintext highlighter-rouge">worker</code>节点故障</strong>,<code class="language-plaintext highlighter-rouge">master</code>节点会周期性的<code class="language-plaintext highlighter-rouge">ping</code>所有的<code class="language-plaintext highlighter-rouge">worker</code>节点,若<code class="language-plaintext highlighter-rouge">worker</code>在给定时间内未响应,则<code class="language-plaintext highlighter-rouge">master</code>会标记<code class="language-plaintext highlighter-rouge">worker</code>为<code class="language-plaintext highlighter-rouge">failure</code>状态。此时,该<code class="language-plaintext highlighter-rouge">worker</code>节点上已执行完的<code class="language-plaintext highlighter-rouge">map task</code>会被重新置为<code class="language-plaintext highlighter-rouge">initial idle</code>状态,然后会等待其它<code class="language-plaintext highlighter-rouge">worker</code>执行此<code class="language-plaintext highlighter-rouge">task</code>。类似的,任何此<code class="language-plaintext highlighter-rouge">worker</code>上正在执行的<code class="language-plaintext highlighter-rouge">map()</code>或<code class="language-plaintext highlighter-rouge">reduce()</code>任务也会被重置为<code class="language-plaintext highlighter-rouge">idle</code>状态,然后等待调度。</p>
<p>为什么已经完成的<code class="language-plaintext highlighter-rouge">map task</code>还要被重新执行呐?因为<code class="language-plaintext highlighter-rouge">map()</code>会将<code class="language-plaintext highlighter-rouge">intermediate data</code>写在本次磁盘上,当<code class="language-plaintext highlighter-rouge">worker</code>不可访问时,执行<code class="language-plaintext highlighter-rouge">reduce()</code>时无法从<code class="language-plaintext highlighter-rouge">failure worker</code>中取数据。而<code class="language-plaintext highlighter-rouge">completed reduce</code>不需要重新执行,因为<code class="language-plaintext highlighter-rouge">reduce()</code>函数已将最终结果写到外部存储<code class="language-plaintext highlighter-rouge">HDFS</code>上。</p>
<p><strong><code class="language-plaintext highlighter-rouge">master</code>节点故障问题</strong>,容错方案较为简单,就是让<code class="language-plaintext highlighter-rouge">master</code>每隔一段时间将<code class="language-plaintext highlighter-rouge">data structures</code>写到磁盘上,做<code class="language-plaintext highlighter-rouge">checkpoint </code>。当<code class="language-plaintext highlighter-rouge">master</code>节点<code class="language-plaintext highlighter-rouge">die</code>后,重新启动一个<code class="language-plaintext highlighter-rouge">master</code>然后读取之前<code class="language-plaintext highlighter-rouge">checkpoint</code>的数据就可恢复状态。</p>
<h3 id="input文件切分split和block的区别">Input文件切分,Split和Block的区别</h3>
<p><code class="language-plaintext highlighter-rouge">split</code>是文件在逻辑上的划分,是程序中的一个独立处理单元,每一个<code class="language-plaintext highlighter-rouge">split</code>分配给一个<code class="language-plaintext highlighter-rouge">task</code>去处理。而在实际的存储系统中,使用<code class="language-plaintext highlighter-rouge">block</code>对文件在物理上进行划分,一个<code class="language-plaintext highlighter-rouge">block</code>的多个备份存储在不同节点上。</p>
<p>文件切分算法主要用于确定<code class="language-plaintext highlighter-rouge">inputSplit</code>的个数及每个<code class="language-plaintext highlighter-rouge">inputSplit</code>对应的数据段,<code class="language-plaintext highlighter-rouge">splitSize=max{ minSize, min{totalSize/numSplits, blockSize}}</code>,最后剩下不足<code class="language-plaintext highlighter-rouge">splitSize</code>的数据块单独成为一个<code class="language-plaintext highlighter-rouge">InputSplit</code>。</p>
<p><code class="language-plaintext highlighter-rouge">Host</code>选择算法,<code class="language-plaintext highlighter-rouge">Input</code>对象由(<code class="language-plaintext highlighter-rouge">file</code>, <code class="language-plaintext highlighter-rouge">start</code>, <code class="language-plaintext highlighter-rouge">length</code>, <code class="language-plaintext highlighter-rouge">hosts</code>)这个四元组构成,节点列表是关键,关系到任务的本地性(<code class="language-plaintext highlighter-rouge">locality</code>),<code class="language-plaintext highlighter-rouge">mapreduce</code>优先让空闲资源处理本节点的数据。</p>
<p><code class="language-plaintext highlighter-rouge">mapreduce</code>的<code class="language-plaintext highlighter-rouge">sort</code>分两种:<code class="language-plaintext highlighter-rouge">map task</code>中<code class="language-plaintext highlighter-rouge">spill</code>数据的排序,数据写入本地磁盘之前,先要对数据进行一次本地排序(快排算法)。<code class="language-plaintext highlighter-rouge">reduce task</code>中数据排序,采用归并排序或小顶堆算法,<code class="language-plaintext highlighter-rouge">sort</code>和<code class="language-plaintext highlighter-rouge">reduce</code>可同时进行。</p>
<h3 id="mapreduce分布式计算框架">MapReduce分布式计算框架</h3>
<p><img src="../../../../resource/2021/mapreduce/map_reduce_phases.jpg" width="700" alt="mapreduce原理概述" /></p>
<p><code class="language-plaintext highlighter-rouge">MapReduce</code>核心组件有<code class="language-plaintext highlighter-rouge">JobTracker</code>、<code class="language-plaintext highlighter-rouge">TaskTracker</code>和<code class="language-plaintext highlighter-rouge">Client</code>:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">JobTracker</code>负责集群资源监控和作业调度,通过心跳监控所有<code class="language-plaintext highlighter-rouge">TaskTracker</code>的健康状况。监控<code class="language-plaintext highlighter-rouge">Job</code>的运行情况、执行进度、资源使用,交由任务调度器负责资源分配,任务调度器有<code class="language-plaintext highlighter-rouge">FIFO Scheduler</code>和<code class="language-plaintext highlighter-rouge">Capacity Scheduler</code>。</li>
<li><code class="language-plaintext highlighter-rouge">TaskTracker</code>具体执行<code class="language-plaintext highlighter-rouge">Task</code>的单元,以<code class="language-plaintext highlighter-rouge">slot</code>为单位等量划分本节点资源,分为<code class="language-plaintext highlighter-rouge">MapSlot</code>和<code class="language-plaintext highlighter-rouge">ReduceSlot</code>。其通过心跳周期性向<code class="language-plaintext highlighter-rouge">JobTracker</code>汇报本节点资源使用情况和任务执行进度,同时接收<code class="language-plaintext highlighter-rouge">JobTracker</code>的命令执行相应的操作(启动新任务、杀死任务等)。</li>
<li><code class="language-plaintext highlighter-rouge">Client</code>提交用户编写的程序到集群,查看<code class="language-plaintext highlighter-rouge">Job</code>的运行状态。</li>
</ul>
<p><img src="../../../../resource/2021/mapreduce/mr_job_lifecycle.jpg" width="700" alt="MR Job声明周期" /></p>
<p><code class="language-plaintext highlighter-rouge">MR Job</code>声明周期文字描述:</p>
<ol>
<li>作业提交和初始化:首先<code class="language-plaintext highlighter-rouge">JobClient</code>将作业相关文件上传到<code class="language-plaintext highlighter-rouge">HDFS</code>,然后<code class="language-plaintext highlighter-rouge">JobClient</code>通知<code class="language-plaintext highlighter-rouge">JobTracker</code>使其对作业进行初始化(<code class="language-plaintext highlighter-rouge">JobInProgress</code>和<code class="language-plaintext highlighter-rouge">TaskInProgress</code>)。</li>
<li>任务调度和监控:<code class="language-plaintext highlighter-rouge">JobTracker</code>的任务调度器按照一定策略(<code class="language-plaintext highlighter-rouge">TaskScheduler</code>),将<code class="language-plaintext highlighter-rouge">task</code>调度到空闲的<code class="language-plaintext highlighter-rouge">TaskTracker</code>。</li>
<li>任务<code class="language-plaintext highlighter-rouge">JVM</code>启动,<code class="language-plaintext highlighter-rouge">TaskTracker</code>下载任务所需文件,并为每个Task启动一个独立的JVM。</li>
<li><code class="language-plaintext highlighter-rouge">TaskTracker</code>启动<code class="language-plaintext highlighter-rouge">Task</code>,<code class="language-plaintext highlighter-rouge">Task</code>通过<code class="language-plaintext highlighter-rouge">RPC</code>将其状态汇报给<code class="language-plaintext highlighter-rouge">TaskTracker</code>,再由<code class="language-plaintext highlighter-rouge">TaskTracker</code>汇报给<code class="language-plaintext highlighter-rouge">JobTracker</code>。</li>
<li>完成作业后,会讲数据回写到<code class="language-plaintext highlighter-rouge">hdfs</code>。</li>
</ol>
大数据的三架马车之HDFS
2021-07-19T00:00:00+00:00
https://dongma.github.io/2021/07/19/apache-hadoop-mechanism
<blockquote>
<p>主要介绍HDFS的基本组成和原理、Hadoop 2.0对HDFS的改进、HADOOP命令和基本API、通过读Google File System论文来理解HDFS设计理念。</p>
</blockquote>
<p><code class="language-plaintext highlighter-rouge">Hadoop</code>是<code class="language-plaintext highlighter-rouge">Apache</code>一个开源的分布式计算平台,核心是以<code class="language-plaintext highlighter-rouge">HDFS</code>分布式文件系统和<code class="language-plaintext highlighter-rouge">MapReduce</code>分布式计算框架组成,为用户提供了一套底层透明的分布式基础设施。</p>
<p><code class="language-plaintext highlighter-rouge">HDFS</code>是<code class="language-plaintext highlighter-rouge">Hadoop</code>分布式文件系统,具有高容错性、高伸缩性,允许用户基于廉价精简部署,构件分布式文件系统,为分布式计算存储提供底层支持。<code class="language-plaintext highlighter-rouge">MapReduce</code>提供简单的<code class="language-plaintext highlighter-rouge">API</code>,允许用户在不了解底层细节的情况下,开发分布式并行程序,利用大规模集群资源,解决传统单机无法解决的大数据处理问题,其设计思想起源<code class="language-plaintext highlighter-rouge">Google GFS</code>、<code class="language-plaintext highlighter-rouge">MapReduce Paper</code>。</p>
<h3 id="在mac上搭建hadoop单机版环境">在Mac上搭建Hadoop单机版环境</h3>
<p>从 https://hadoop.apache.org 下载二进制的安装包,具体配置可进行<code class="language-plaintext highlighter-rouge">Google</code>。配置完成后,在执行<code class="language-plaintext highlighter-rouge">HDFS</code>命令时会提示 <code class="language-plaintext highlighter-rouge">Unable to load native-hadoop library for your platform...using buildin-java classes..</code>,运行<code class="language-plaintext highlighter-rouge">Hadoop</code>的二进制包与当前平台不兼容。
<!-- more -->
为解决该问题,需在机器上编译<code class="language-plaintext highlighter-rouge">Hadoop</code>的源码包,用编译生成的<code class="language-plaintext highlighter-rouge">native library</code>替换二进制包中的相同文件。编译<code class="language-plaintext highlighter-rouge">Hadoop</code>源码需安装<code class="language-plaintext highlighter-rouge">cmake</code>、<code class="language-plaintext highlighter-rouge">protobuf</code>、<code class="language-plaintext highlighter-rouge">maven</code>、<code class="language-plaintext highlighter-rouge">openssl</code>组件。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>mvn package <span class="nt">-Pdist</span>,native <span class="nt">-DskipTests</span> <span class="nt">-Dtar</span>
</code></pre></div></div>
<p>在编译hadoop-2.10.1的<code class="language-plaintext highlighter-rouge">hadoop-pipes</code>模块时出现错误,原因是由于<code class="language-plaintext highlighter-rouge">openssl</code>的版本不兼容,机器上的是<code class="language-plaintext highlighter-rouge">32</code>位,而实际需要<code class="language-plaintext highlighter-rouge">64</code>位。最后从github下载<code class="language-plaintext highlighter-rouge">openssl-1.0.2q.tar.gz</code>安装包,通过源码安装,并在/etc/profile中配置环境变量:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">OPENSSL_ROOT_DIR</span><span class="o">=</span>/usr/local/Cellar/openssl@1.0.2q
<span class="nb">export </span><span class="nv">OPENSSL_INCLUDE_DIR</span><span class="o">=</span>/usr/local/Cellar/openssl@1.0.2q/include/
</code></pre></div></div>
<p>然后重新执行<code class="language-plaintext highlighter-rouge">maven</code>命令,<code class="language-plaintext highlighter-rouge">hadoop</code>源码编译通过了。最后将<code class="language-plaintext highlighter-rouge">hadoop-dist</code>目录下的<code class="language-plaintext highlighter-rouge">native</code>包拷贝到<code class="language-plaintext highlighter-rouge">hadoop</code>二进制的源码包下就可以了。</p>
<h3 id="hadoop-10架构">Hadoop 1.0架构</h3>
<p><code class="language-plaintext highlighter-rouge">GFS cluster</code>由一个<code class="language-plaintext highlighter-rouge">master</code>节点和多个<code class="language-plaintext highlighter-rouge">chunkserver</code>节点组成,多个<code class="language-plaintext highlighter-rouge">GFS client</code>可以对其进行访问,其中每一个通常都是运行用户级服务器进程的商用<code class="language-plaintext highlighter-rouge">linux</code>机器。大文件会被分为大小固定为<code class="language-plaintext highlighter-rouge">64MB</code>的块。</p>
<p><img src="../../../../resource/2021/hadoop/hadoop-architecture.jpg" width="900" alt="Hadoop 1.0架构图" /></p>
<p><code class="language-plaintext highlighter-rouge">HDFS 1.0</code>中的角色划分:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">NameNode</code>:对应论文中的<code class="language-plaintext highlighter-rouge">GFS master</code>,<code class="language-plaintext highlighter-rouge">NN</code>维护整个文件系统的文件目录树,文件目录的元信息和文件数据块索引;元数据镜像<code class="language-plaintext highlighter-rouge">FsImage</code>和操作日志<code class="language-plaintext highlighter-rouge">EditLog</code>存储在本地,但整个系统存在单点问题,存在<code class="language-plaintext highlighter-rouge">SPOF(Simple Point Of Filure)</code>。</li>
<li><code class="language-plaintext highlighter-rouge">SecondNameNode</code>:又名<code class="language-plaintext highlighter-rouge">CheckPoint Node</code>用于定期合并<code class="language-plaintext highlighter-rouge">FsImage</code>和<code class="language-plaintext highlighter-rouge">EditLog</code>文件,其不接收客户端的请求,作为<code class="language-plaintext highlighter-rouge">NameNode</code>的冷备份。</li>
<li><code class="language-plaintext highlighter-rouge">DataNode</code>:对应<code class="language-plaintext highlighter-rouge">GFS</code>中的<code class="language-plaintext highlighter-rouge">chunkserver</code>,实际的数据存储单元(以<code class="language-plaintext highlighter-rouge">Block</code>为单位),数据以普通文件形式保存在本地文件系统。</li>
<li><code class="language-plaintext highlighter-rouge">Client</code>:与<code class="language-plaintext highlighter-rouge">HDFS</code>交互,进行读写、创建目录、创建文件、复制、删除等操作。<code class="language-plaintext highlighter-rouge">HDFS</code>提供了多种客户端,命令行<code class="language-plaintext highlighter-rouge">shell</code>、<code class="language-plaintext highlighter-rouge">Java api</code>、<code class="language-plaintext highlighter-rouge">Thrift</code>接口、<code class="language-plaintext highlighter-rouge">C library</code>、<code class="language-plaintext highlighter-rouge">WebHDFS</code>等。</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">HDFS</code>的<code class="language-plaintext highlighter-rouge">chunk size</code>大小为<code class="language-plaintext highlighter-rouge">64MB</code>,这比大多数文件系统的<code class="language-plaintext highlighter-rouge">block</code>大小要大。较大的<code class="language-plaintext highlighter-rouge">block size</code>优势在于,在获取块位置信息时候,减少了<code class="language-plaintext highlighter-rouge">client</code>与<code class="language-plaintext highlighter-rouge">NameNode</code>交互的次数。其次,由于在大的<code class="language-plaintext highlighter-rouge">block</code>上,客户端更有可能在给定块上执行许多操作,可以与<code class="language-plaintext highlighter-rouge">NameNode</code>保持一个长时间的<code class="language-plaintext highlighter-rouge">TCP</code>连接来减少网络开销。第三,减少了存储在<code class="language-plaintext highlighter-rouge">NameNode</code>上的元数据的大小,这就可以使得<code class="language-plaintext highlighter-rouge">NameNode</code>将元数据信息保存在<code class="language-plaintext highlighter-rouge">Memory</code>中。</p>
<h3 id="hdfs-metadata元数据信息">HDFS Metadata元数据信息</h3>
<p><code class="language-plaintext highlighter-rouge">GFS</code>论文中<code class="language-plaintext highlighter-rouge">Master</code>节点中存储了三种元数据信息:文件和数据块的<code class="language-plaintext highlighter-rouge">namespace</code>、从<code class="language-plaintext highlighter-rouge">files</code>文件到<code class="language-plaintext highlighter-rouge">chunkserver</code>的映射关系及<code class="language-plaintext highlighter-rouge">chunk</code>副本数据位置。前两种数据是通过<code class="language-plaintext highlighter-rouge">EditLog</code>存储在本地磁盘的,而<code class="language-plaintext highlighter-rouge">chunk location</code>则是在<code class="language-plaintext highlighter-rouge">Master</code>启动时向<code class="language-plaintext highlighter-rouge">chunk server</code>发起请求进行获取。</p>
<p>一个大文件由多个<code class="language-plaintext highlighter-rouge">Data Block</code>数据集合组成,每个数据块在本地文件系统中是以单独的文件存储的。谈谈数据块分布,默认布局规则(假设复制因子为3):</p>
<ul>
<li>第一份拷贝写入创建文件的节点(快速写入数据);</li>
<li>第二份拷贝写入同一个<code class="language-plaintext highlighter-rouge">rack</code>内的节点;</li>
<li>第三份拷贝写入位于不同<code class="language-plaintext highlighter-rouge">rack</code>的节点(应对交换机故障);</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">HDFS</code>写流程,对于大文件,与<code class="language-plaintext highlighter-rouge">HDFS</code>客户端进行交互,<code class="language-plaintext highlighter-rouge">NN</code>告知客户端第一个<code class="language-plaintext highlighter-rouge">Block</code>放在何处?将数据块流式的传输到另外两个数据节点。<code class="language-plaintext highlighter-rouge">FsImage</code>和<code class="language-plaintext highlighter-rouge">EditLog</code>组件的目的:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">NameNode</code>的内存中有整个文件系统的元数据,例如目录树、块信息、权限信息等,当<code class="language-plaintext highlighter-rouge">NameNode</code>宕机时,内存中的元数据将全部丢失。为了让重启的<code class="language-plaintext highlighter-rouge">NameNode</code>获得最新的宕机前的元数据,才有了<code class="language-plaintext highlighter-rouge">FsImage</code>和<code class="language-plaintext highlighter-rouge">EditLog</code>。</li>
<li><code class="language-plaintext highlighter-rouge">FsImage</code>是整个<code class="language-plaintext highlighter-rouge">NameNode</code>内存中元数据在某一时刻的快照(<code class="language-plaintext highlighter-rouge">Snapshot</code>),<code class="language-plaintext highlighter-rouge">FsImage</code>不能频繁的构建,生成<code class="language-plaintext highlighter-rouge">FsImage</code>需要花费大量的内存,目前<code class="language-plaintext highlighter-rouge">FsImage</code>只在<code class="language-plaintext highlighter-rouge">NameNode</code>重启才构建。</li>
<li>而<code class="language-plaintext highlighter-rouge">EditLog</code>则记录的是从这个快照开始到当前所有的元数据的改动。如果<code class="language-plaintext highlighter-rouge">EditLog</code>太多,重放<code class="language-plaintext highlighter-rouge">EditLog</code>会消耗大量的时间,这会导致启动<code class="language-plaintext highlighter-rouge">NameNode</code>花费数小时之久。</li>
</ul>
<p>为了解决以上问题,引入了<code class="language-plaintext highlighter-rouge">Second NameNode</code>组件,我们需要一个机制来帮助我们减少<code class="language-plaintext highlighter-rouge">EditLog</code>文件的大小和构建<code class="language-plaintext highlighter-rouge">fsimage</code>以减少<code class="language-plaintext highlighter-rouge">NameNode</code>的压力。这与<code class="language-plaintext highlighter-rouge">windows</code>的恢复点比较像,允许我们对<code class="language-plaintext highlighter-rouge">OS</code>进行快照。</p>
<h3 id="hdfs数据读写流程">HDFS数据读写流程</h3>
<p><code class="language-plaintext highlighter-rouge">HDFS</code>设计目标是减少<code class="language-plaintext highlighter-rouge">Master</code>参与各种数据操作,在这种背景下,描述一下<code class="language-plaintext highlighter-rouge">client</code>、<code class="language-plaintext highlighter-rouge">master</code>和<code class="language-plaintext highlighter-rouge">chunkserver</code>如何进行交互来实现数据交互、原子性记录追加。
<img src="../../../../resource/2021/hadoop/gfs_client_write_control_and_dataflow.jpg" width="600" alt="hdfs数据读写流程" /></p>
<ol>
<li><code class="language-plaintext highlighter-rouge">client</code>向<code class="language-plaintext highlighter-rouge">master</code>发起请求询问哪个<code class="language-plaintext highlighter-rouge">chunkserver</code>持有当要写入的块及当前数据块的副本位置?<code class="language-plaintext highlighter-rouge">master</code>用<code class="language-plaintext highlighter-rouge">primary</code>标识符以及对应副本位置返回给<code class="language-plaintext highlighter-rouge">client</code>以进行<code class="language-plaintext highlighter-rouge">cache</code>(失效后会再次向<code class="language-plaintext highlighter-rouge">master</code>发起请求);</li>
<li><code class="language-plaintext highlighter-rouge">client</code>将数据写入到所有的副本中(不分先后顺序),每个<code class="language-plaintext highlighter-rouge">chunkserver</code>都会将数据写入内部的<code class="language-plaintext highlighter-rouge">LRU buffer</code>中直到数据被访问或过期;</li>
<li>一旦所有的副本确认已经收到了数据,<code class="language-plaintext highlighter-rouge">client</code>会发送一个<code class="language-plaintext highlighter-rouge">write request</code>到<code class="language-plaintext highlighter-rouge">primary</code>,说明之前的数据已完全写入完成。<code class="language-plaintext highlighter-rouge">primary replica</code>会返回一个连续的流水号给<code class="language-plaintext highlighter-rouge">client</code>;</li>
<li><code class="language-plaintext highlighter-rouge">primary replica</code>将<code class="language-plaintext highlighter-rouge">write</code>请求转发到所有的副本,每一个副本按照<code class="language-plaintext highlighter-rouge">serial number</code>的顺序执行变更,所有副本给<code class="language-plaintext highlighter-rouge">primary</code>返回结果则表示它们已经完成了操作。</li>
<li><code class="language-plaintext highlighter-rouge">primary</code>将信息返给<code class="language-plaintext highlighter-rouge">client</code>,包括<code class="language-plaintext highlighter-rouge">replica</code>在执行操作时发生的<code class="language-plaintext highlighter-rouge">error</code>。</li>
</ol>
<p><code class="language-plaintext highlighter-rouge">DataFlow</code>数据流转的过程,<code class="language-plaintext highlighter-rouge">data</code>是被线性的在一系列的<code class="language-plaintext highlighter-rouge">chunkserver</code>之间进行推送,而不是其它那些通过<code class="language-plaintext highlighter-rouge">topology</code>进行分发。这样做是为了尽量地避免<code class="language-plaintext highlighter-rouge">network bottlenecks</code>及<code class="language-plaintext highlighter-rouge">high-latency links</code>问题。举个例子,<code class="language-plaintext highlighter-rouge">client</code>推送数据到<code class="language-plaintext highlighter-rouge">chunkserver S1</code>, <code class="language-plaintext highlighter-rouge">S1</code>会将数据推送给离它最近的<code class="language-plaintext highlighter-rouge">chunkserver S2或S3</code>。本质是通过<code class="language-plaintext highlighter-rouge">IP address</code>之间距离来判断,<code class="language-plaintext highlighter-rouge">network之间的hops</code>。此外,数据的传输是通过<code class="language-plaintext highlighter-rouge">TCP</code>连接来完成的,一旦<code class="language-plaintext highlighter-rouge">chunkserver</code>收到一些数据,它会立刻进行数据转发。</p>
<h3 id="hadoop-20对hdfs的改进">Hadoop 2.0对HDFS的改进</h3>
<p><code class="language-plaintext highlighter-rouge">Hdfs 1.0</code>的问题:<code class="language-plaintext highlighter-rouge">NameNode SPOF</code>问题,<code class="language-plaintext highlighter-rouge">NameNode</code>挂掉了整个集群不可用,此外,<code class="language-plaintext highlighter-rouge">Name Node</code>内存受限,整个集群的<code class="language-plaintext highlighter-rouge">size</code>受限于<code class="language-plaintext highlighter-rouge">NameNode</code>的内存空间。<code class="language-plaintext highlighter-rouge">Hadoop 2.0</code>的解决方案,<code class="language-plaintext highlighter-rouge">HDFS HA</code>提供名称节点热备机制,<code class="language-plaintext highlighter-rouge">HDFS Federation</code>管理多个命名空间。</p>
<p><code class="language-plaintext highlighter-rouge">NameNode HA</code>设计思路</p>
<ul>
<li>如何实现主和备<code class="language-plaintext highlighter-rouge">NameNode</code>状态同步,主备一致性?</li>
<li>脑裂的解决,集群产生了两个<code class="language-plaintext highlighter-rouge">leader</code>导致集群行为不一致,仲裁以及<code class="language-plaintext highlighter-rouge">fencing</code>的方式。</li>
<li>透明切换(<code class="language-plaintext highlighter-rouge">failover</code>),<code class="language-plaintext highlighter-rouge">NameNode</code>切换对外透明,当主<code class="language-plaintext highlighter-rouge">NameNode</code>切换到另一台机器时,不应该导致正在连接的<code class="language-plaintext highlighter-rouge">Client</code>,<code class="language-plaintext highlighter-rouge">DataNode</code>失效。</li>
</ul>
<ol>
<li>对于<code class="language-plaintext highlighter-rouge">NameNode</code>主备一致实现,<code class="language-plaintext highlighter-rouge">Active NameNode</code>启动后提供服务,并把<code class="language-plaintext highlighter-rouge">EditLog</code>写入到本地和<code class="language-plaintext highlighter-rouge">QJM*</code>中,<code class="language-plaintext highlighter-rouge">Standby NameNode</code>周期性的从<code class="language-plaintext highlighter-rouge">QJM</code>中拉取<code class="language-plaintext highlighter-rouge">EditLog</code>,保持与<code class="language-plaintext highlighter-rouge">active</code>的状态一致。<code class="language-plaintext highlighter-rouge">DataNode</code>同时向两个<code class="language-plaintext highlighter-rouge">NameNode</code>发送<code class="language-plaintext highlighter-rouge">BlockReport</code>。</li>
<li><code class="language-plaintext highlighter-rouge">HA</code>之脑裂的解决,<code class="language-plaintext highlighter-rouge">QJM</code>的<code class="language-plaintext highlighter-rouge">fencing</code>,确保只有一个<code class="language-plaintext highlighter-rouge">NN</code>能成功。<code class="language-plaintext highlighter-rouge">DataNode</code>的<code class="language-plaintext highlighter-rouge">fencing</code>,确保只有一个<code class="language-plaintext highlighter-rouge">NN</code>能命令<code class="language-plaintext highlighter-rouge">DN</code>。每个<code class="language-plaintext highlighter-rouge">NN</code>改变状态的时候,会向<code class="language-plaintext highlighter-rouge">DN</code>发送自己的状态和一个序列号(类似<code class="language-plaintext highlighter-rouge">Epoch Numbers</code>)。当收到<code class="language-plaintext highlighter-rouge">NN</code>提供了更大序列号时,<code class="language-plaintext highlighter-rouge">DN</code>更新序列号,之后只接收新<code class="language-plaintext highlighter-rouge">NN</code>的命令。</li>
<li>主备切换的实现<code class="language-plaintext highlighter-rouge">ZKFC</code>,作为独立的进程存在,负责控制<code class="language-plaintext highlighter-rouge">NameNode</code>的主备切换,<code class="language-plaintext highlighter-rouge">ZKFC</code>会监测<code class="language-plaintext highlighter-rouge">NameNode</code>的健康状况,当<code class="language-plaintext highlighter-rouge">Active NameNode</code>出现异常时会通过<code class="language-plaintext highlighter-rouge">Zookeeper</code>集群进行一次主备选举。</li>
</ol>
k8s核心组件及pod组件间通信原理
2021-04-25T00:00:00+00:00
https://dongma.github.io/2021/04/25/k8s-pods-and-network
<blockquote>
<p>介绍<code class="language-plaintext highlighter-rouge">k8s</code>的核心组件如<code class="language-plaintext highlighter-rouge">Pod</code>、<code class="language-plaintext highlighter-rouge">Controller</code>、<code class="language-plaintext highlighter-rouge">StatefulSet</code>等组件以及组件间通信原理<code class="language-plaintext highlighter-rouge">Service</code>及<code class="language-plaintext highlighter-rouge">Ingress</code>服务。</p>
</blockquote>
<h3 id="docker实例及pods间的通信原理">Docker实例及Pods间的通信原理</h3>
<p>在通信协议中“网络栈”包括有:网卡(<code class="language-plaintext highlighter-rouge">network interface</code>)、回环设备(<code class="language-plaintext highlighter-rouge">loopback device</code>)、路由表(<code class="language-plaintext highlighter-rouge">routing table</code>)和<code class="language-plaintext highlighter-rouge">iptables</code>规则。在<code class="language-plaintext highlighter-rouge">docker</code>中启动一个容器可使用宿主机的网络栈(<code class="language-plaintext highlighter-rouge">-net=host</code>),指定<code class="language-plaintext highlighter-rouge">-net</code>后默认不开启<code class="language-plaintext highlighter-rouge">network namespace</code>空间:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run –d –net<span class="o">=</span>host <span class="nt">--name</span> nginx-host nginx
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">nginx</code>服务启动后默认监听主机<code class="language-plaintext highlighter-rouge">80</code>端口,容器启动后会创建一个<code class="language-plaintext highlighter-rouge">docker0</code>的网桥。<code class="language-plaintext highlighter-rouge">docker</code>实例通过<code class="language-plaintext highlighter-rouge">Veth Pair</code>与宿主机建立连接关系,其中<code class="language-plaintext highlighter-rouge">Veth</code>的一端在容器内,另一段插在宿主机的<code class="language-plaintext highlighter-rouge">docker0</code>网桥上。
<!-- more -->
同一台宿主机上的容器实例间的网络是互通的,请求路由是通过宿主机向外转发。<code class="language-plaintext highlighter-rouge">ping 172.17.0.3</code>时匹配<code class="language-plaintext highlighter-rouge">0.0.0.0</code>的路由网关,意味着这是一条直连规则,匹配该规则的都走主机的<code class="language-plaintext highlighter-rouge">eth0</code>网卡。</p>
<p>在容器内<code class="language-plaintext highlighter-rouge">ping other-ip</code>时需将<code class="language-plaintext highlighter-rouge">other-ip</code>转换为<code class="language-plaintext highlighter-rouge">mac</code>地址(通<code class="language-plaintext highlighter-rouge">arp</code>地址解析获取硬件地址),容器内无法完成此操作容器通过默认路由在宿主机解析,获取请求<code class="language-plaintext highlighter-rouge">mac</code>地址 然后从容器经过<code class="language-plaintext highlighter-rouge">docker0</code>中 <code class="language-plaintext highlighter-rouge">Veth Pair</code>另外一端通过宿主机将请求转发出去。</p>
<p>在<code class="language-plaintext highlighter-rouge">docker</code>的默认配置下,一台宿主机上的<code class="language-plaintext highlighter-rouge">docker0</code>网桥,和其他宿主机上的<code class="language-plaintext highlighter-rouge">docker0</code>网桥,没有任何关联,它们互相之间也没办法连通。所以,连接在这些网桥上的容器,自然也没办法进行通信了。</p>
<h4 id="1-容器跨主机网络overlay-network">1. 容器跨主机网络(Overlay Network)</h4>
<p><code class="language-plaintext highlighter-rouge">flannel</code> 项目是<code class="language-plaintext highlighter-rouge">coreOS</code>公司主推的容器网络方案,事实上,<code class="language-plaintext highlighter-rouge">flannel</code>项目本身只是一个框架,真正为我们提供容器网络功能的,是 <code class="language-plaintext highlighter-rouge">flannel</code>的后端实现。有<code class="language-plaintext highlighter-rouge">3</code>种方式,基于<code class="language-plaintext highlighter-rouge">vxlan</code>、<code class="language-plaintext highlighter-rouge">host-gw</code>和<code class="language-plaintext highlighter-rouge">udp</code>进行实现。<code class="language-plaintext highlighter-rouge">flannel UDP</code>模式提供的其实是一个三层的<code class="language-plaintext highlighter-rouge">Overlay</code>网络。</p>
<p><code class="language-plaintext highlighter-rouge">node 1</code>上有一个容器<code class="language-plaintext highlighter-rouge">container-1</code>,它的<code class="language-plaintext highlighter-rouge">IP</code>地址是<code class="language-plaintext highlighter-rouge">100.96.1.2</code>,对应的<code class="language-plaintext highlighter-rouge">docker0</code>网桥的地址是<code class="language-plaintext highlighter-rouge">100.96.1.1/24</code>。</p>
<p><code class="language-plaintext highlighter-rouge">node 2</code>上有一个容器<code class="language-plaintext highlighter-rouge">container-2</code>,它的<code class="language-plaintext highlighter-rouge">IP</code>地址是<code class="language-plaintext highlighter-rouge">100.96.2.3</code>,对应的<code class="language-plaintext highlighter-rouge">docker0</code>网桥的地址是<code class="language-plaintext highlighter-rouge">100.96.2.1/24</code>。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip route
default via 10.168.0.1 dev eth0
100.96.0.0/16 dev flannel0 proto kernel scope <span class="nb">link </span>src 100.96.1.0
100.96.1.0/24 dev docker0 proto kernel scope <span class="nb">link </span>src 100.96.1.1
10.168.0.0/24 dev eth0 proto kernel scope <span class="nb">link </span>src 10.168.0.2
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">node</code>跨主机通信引入了<code class="language-plaintext highlighter-rouge">flannel</code>组件,从<code class="language-plaintext highlighter-rouge">node1</code>请求<code class="language-plaintext highlighter-rouge">node2</code>在每个组件对请求包进行分发(<code class="language-plaintext highlighter-rouge">docker0</code>、<code class="language-plaintext highlighter-rouge">flannel</code>),<code class="language-plaintext highlighter-rouge">flannel</code>包含子网在<code class="language-plaintext highlighter-rouge">node2</code>地址在<code class="language-plaintext highlighter-rouge">subnet</code>范围内,则对请求包进行分发最终达到<code class="language-plaintext highlighter-rouge">node2</code>上的<code class="language-plaintext highlighter-rouge">container</code>。</p>
<p><code class="language-plaintext highlighter-rouge">flannel</code>项目里一个非常重要的概念子网(<code class="language-plaintext highlighter-rouge">subnet</code>),在由<code class="language-plaintext highlighter-rouge">flannel</code>管理的容器网络里,一台宿主机上的所有容器,都属于该宿主机被分配的一个“子网”。在我们的例子中,<code class="language-plaintext highlighter-rouge">node 1</code> 的子网是<code class="language-plaintext highlighter-rouge">100.96.1.0/24</code>,<code class="language-plaintext highlighter-rouge">container-1</code> 的<code class="language-plaintext highlighter-rouge">IP</code>地址是<code class="language-plaintext highlighter-rouge">100.96.1.2</code>。<code class="language-plaintext highlighter-rouge">node 2</code>的子网是 <code class="language-plaintext highlighter-rouge">100.96.2.0/24</code>,<code class="language-plaintext highlighter-rouge">container-2</code>的 IP地址是<code class="language-plaintext highlighter-rouge">100.96.2.3</code>。</p>
<p><code class="language-plaintext highlighter-rouge">TUN</code>设备的原理,这正是一个从用户态向内核态的流动方向(<code class="language-plaintext highlighter-rouge">Flannel</code>进程向<code class="language-plaintext highlighter-rouge">TUN</code>设备发送数据包),所以 <code class="language-plaintext highlighter-rouge">Linux</code>内核网络栈就会负责处理这个<code class="language-plaintext highlighter-rouge">IP</code>包,具体的处理方法,就是通过本机的路由表来寻找这个<code class="language-plaintext highlighter-rouge">IP</code>包的下一步流向。</p>
<p>课后问题:我觉得不合适,<code class="language-plaintext highlighter-rouge">mac</code>地址为硬件地址 当与请求节点直连时 可通过<code class="language-plaintext highlighter-rouge">mac</code>实现,但当目的<code class="language-plaintext highlighter-rouge">node</code>不在<code class="language-plaintext highlighter-rouge">subnet</code>时,还是要需要<code class="language-plaintext highlighter-rouge">rarp</code>地址逆解析)转换为<code class="language-plaintext highlighter-rouge">ip</code> 然后将数据包分发到目的<code class="language-plaintext highlighter-rouge">node</code>。</p>
<p><code class="language-plaintext highlighter-rouge">VXLAN</code>,即 <code class="language-plaintext highlighter-rouge">Virtual Extensible LAN</code>(虚拟可扩展局域网),是 <code class="language-plaintext highlighter-rouge">Linux </code>内核本身就支持的一种网络虚似化技术。所以说,<code class="language-plaintext highlighter-rouge">VXLAN</code> 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面相似的“隧道”机制,构建出覆盖网络(<code class="language-plaintext highlighter-rouge">Overlay Network</code>)。</p>
<p>设计思想是在现有的三层网络之上,“覆盖”一层虚拟的、由内核 <code class="language-plaintext highlighter-rouge">VXLAN</code> 模块负责维护的二层网络,使得连接在这个 <code class="language-plaintext highlighter-rouge">VXLAN </code>二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(<code class="language-plaintext highlighter-rouge">LAN</code>)里那样自由通信。</p>
<h4 id="2-kubernetes的网络模型与cni网络插件">2. kubernetes的网络模型与CNI网络插件</h4>
<p><code class="language-plaintext highlighter-rouge">kubernetes</code>使用<code class="language-plaintext highlighter-rouge">cni</code>作为<code class="language-plaintext highlighter-rouge">pod</code>的容器间通信的网桥(与<code class="language-plaintext highlighter-rouge">docker0</code>功能相同),初始化<code class="language-plaintext highlighter-rouge">pod</code>网络流程:</p>
<p>创建<code class="language-plaintext highlighter-rouge">Infra</code>容器调用<code class="language-plaintext highlighter-rouge">cni</code>插件初始化<code class="language-plaintext highlighter-rouge">infra</code>容器网络(插件位置:<code class="language-plaintext highlighter-rouge">/opt/cni/bin/flannel</code>),开始<code class="language-plaintext highlighter-rouge">dockershim</code>设置的一组 <code class="language-plaintext highlighter-rouge">CNI</code>环境变量(枚举值<code class="language-plaintext highlighter-rouge">ADD</code>、<code class="language-plaintext highlighter-rouge">DELETE</code>),用于表示将容器的<code class="language-plaintext highlighter-rouge">VethPair</code>插入或从<code class="language-plaintext highlighter-rouge">cni0</code>网桥移除。
与此同时,<code class="language-plaintext highlighter-rouge">cni bridge</code>插件检查<code class="language-plaintext highlighter-rouge">cni</code>网桥在宿主机上是否存在,若不存在则进行创建。接着,<code class="language-plaintext highlighter-rouge">cni bridge</code>插件在<code class="language-plaintext highlighter-rouge">network namespace</code>创建<code class="language-plaintext highlighter-rouge">VethPair</code>,将其中一端插入到宿主机的<code class="language-plaintext highlighter-rouge">cni0</code>网桥,另一端直接赋予容器实例<code class="language-plaintext highlighter-rouge">eth0</code>,<code class="language-plaintext highlighter-rouge">cni</code>插件把容器<code class="language-plaintext highlighter-rouge">ip</code>提供给<code class="language-plaintext highlighter-rouge">dockershim</code> 被<code class="language-plaintext highlighter-rouge">kubelet</code>用于添加到<code class="language-plaintext highlighter-rouge">pod</code>的<code class="language-plaintext highlighter-rouge">status</code>字段。</p>
<p>接下来,<code class="language-plaintext highlighter-rouge">cni bridge</code>调用<code class="language-plaintext highlighter-rouge">cni ipam</code>插件 从<code class="language-plaintext highlighter-rouge">ipam.subnet</code>子网中给容器<code class="language-plaintext highlighter-rouge">eth0</code>网卡分配<code class="language-plaintext highlighter-rouge">ip</code>地址同时设置<code class="language-plaintext highlighter-rouge">default route</code>配置,最后<code class="language-plaintext highlighter-rouge">cni bridge</code>插件为<code class="language-plaintext highlighter-rouge">cni</code>网桥设置<code class="language-plaintext highlighter-rouge">ip</code>地址。</p>
<p>三层网络特点:通过<code class="language-plaintext highlighter-rouge">ip route</code>得到数据传输路由,跨节点传输<code class="language-plaintext highlighter-rouge">ip</code>包时会将<code class="language-plaintext highlighter-rouge">route</code>中<code class="language-plaintext highlighter-rouge">geteway</code>的<code class="language-plaintext highlighter-rouge">mac</code>地址作为<code class="language-plaintext highlighter-rouge">ip</code>包的请求头用于数据包传输,到达目标<code class="language-plaintext highlighter-rouge">node</code>时 进行拆包,然后根据<code class="language-plaintext highlighter-rouge">ip</code>包去除<code class="language-plaintext highlighter-rouge">dest</code>地址并根据当前<code class="language-plaintext highlighter-rouge">node</code>的<code class="language-plaintext highlighter-rouge">route</code>列表,将数据包转发到相应<code class="language-plaintext highlighter-rouge">container</code>中。
优缺点:避免了额外的封包、拆包操作 性能较好,但要求集群宿主机间是二层连通的;
隧道模式:隧道模式通过<code class="language-plaintext highlighter-rouge">BGP</code>维护路由关系,其会将集群节点的<code class="language-plaintext highlighter-rouge">ip</code> 对应<code class="language-plaintext highlighter-rouge">gateway</code> 保存在当前节点的路由中,在请求发包时数据包<code class="language-plaintext highlighter-rouge">mac</code>头地址指定为路由<code class="language-plaintext highlighter-rouge">gateway</code>地址。
优缺点:需维护集群中所有<code class="language-plaintext highlighter-rouge">container</code>的连接信息,当集群中容器数量较大时<code class="language-plaintext highlighter-rouge">BGP</code>会爆炸增长,此时可切换至集群中某几个节点维护网络关系,剩余的节点从主要节点同步路由信息。</p>
<p><code class="language-plaintext highlighter-rouge">k8s</code>使用<code class="language-plaintext highlighter-rouge">NetworkPolicy</code>定义<code class="language-plaintext highlighter-rouge">pod</code>的隔离机制,使用<code class="language-plaintext highlighter-rouge">ingress</code>和<code class="language-plaintext highlighter-rouge">egress</code>定义访问策略(限制可请求的<code class="language-plaintext highlighter-rouge">pod</code>及<code class="language-plaintext highlighter-rouge">namespace</code>、<code class="language-plaintext highlighter-rouge">port</code>端口),其本质上是<code class="language-plaintext highlighter-rouge">k8s</code>网络插件在宿主机上生成了<code class="language-plaintext highlighter-rouge">iptables</code>路由规则;</p>
<h3 id="容器编排和kubernetes作业管理">容器编排和Kubernetes作业管理</h3>
<p>随笔写一下,<code class="language-plaintext highlighter-rouge">K8S</code>中<code class="language-plaintext highlighter-rouge">pod</code>的概念,其本质是用来解决一系列容器的进程组问题。生产环境中,往往部署的多个<code class="language-plaintext highlighter-rouge">docker</code>实例间具有亲密性关系,类似于操作系统中进程组的概念。</p>
<p><code class="language-plaintext highlighter-rouge">Pod</code>是<code class="language-plaintext highlighter-rouge">K8s</code>中最小编排单位,将这个设计落实到<code class="language-plaintext highlighter-rouge">API</code>对象上,<code class="language-plaintext highlighter-rouge">Pod</code> 扮演的是传统部署环境里“虚拟机”的角色,把容器看作是运行在这个“机器”里的“用户程序”。比如,凡是调度、网络、存储,以及安全相关的属性,基本上是 <code class="language-plaintext highlighter-rouge">Pod</code> 级别的。</p>
<p>在<code class="language-plaintext highlighter-rouge">Pod</code>的实现需要使用一个中间容器,这个容器叫作<code class="language-plaintext highlighter-rouge">Infra</code>容器。而其他用户定义的容器,则通过<code class="language-plaintext highlighter-rouge"> Join Network Namespace </code>的方式,与 <code class="language-plaintext highlighter-rouge">Infra</code> 容器关联在一起。</p>
<p><code class="language-plaintext highlighter-rouge">Pod</code>的进阶使用中有一些高级组件,<code class="language-plaintext highlighter-rouge">Secret</code>、<code class="language-plaintext highlighter-rouge">ConfigMap</code>、<code class="language-plaintext highlighter-rouge">Downward API</code>和<code class="language-plaintext highlighter-rouge">ServiceAccountToken</code>组件,<code class="language-plaintext highlighter-rouge">Secret</code>的作用,是帮你把<code class="language-plaintext highlighter-rouge">Pod</code>想要访问的加密数据,存放到<code class="language-plaintext highlighter-rouge">Etcd</code>中。然后,你就可以通过在<code class="language-plaintext highlighter-rouge">Pod</code>的容器里挂载<code class="language-plaintext highlighter-rouge">Volume</code>的方式,访问到这些<code class="language-plaintext highlighter-rouge">Secret</code>里保存的信息了。</p>
<p><code class="language-plaintext highlighter-rouge">ConfigMap</code>保存的是不需要加密的、应用所需的配置信息。你可以使用<code class="language-plaintext highlighter-rouge">kubectl create configmap</code>从文件或者目录创建<code class="language-plaintext highlighter-rouge">ConfigMap</code>,也可以直接编写<code class="language-plaintext highlighter-rouge">ConfigMap</code>对象的<code class="language-plaintext highlighter-rouge">YAML</code>文件。</p>
<p><code class="language-plaintext highlighter-rouge">Deployment</code>是控制器组件,其定义编排比较简单,确保携带了<code class="language-plaintext highlighter-rouge">app=nginx</code>标签的<code class="language-plaintext highlighter-rouge">pod</code>的个数,永远等于<code class="language-plaintext highlighter-rouge">spec.replicas</code>指定的个数。它实现了<code class="language-plaintext highlighter-rouge">Kubernetes</code> 项目中一个非常重要的功能:<code class="language-plaintext highlighter-rouge">Pod</code> 的“水平扩展 / 收缩”(<code class="language-plaintext highlighter-rouge">horizontal scaling out/in</code>)。这个功能,是从<code class="language-plaintext highlighter-rouge">PaaS</code>时代开始,一个平台级项目就必须具备的编排能力。</p>
<p><code class="language-plaintext highlighter-rouge">Deployment</code>并不是直接操作<code class="language-plaintext highlighter-rouge">Pod</code>的,而是通过<code class="language-plaintext highlighter-rouge">ReplicaSet</code>进行管理。一个<code class="language-plaintext highlighter-rouge">ReplicaSet</code> 对象,其实就是由副本数目的定义和一个 <code class="language-plaintext highlighter-rouge">Pod</code>模板组成的。不难发现,它的定义其实是<code class="language-plaintext highlighter-rouge">Deployment</code>的一个子集。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl scale deployment nginx-deployment <span class="nt">--replicas</span><span class="o">=</span>4deployment.apps/nginx-deployment scaled
<span class="nv">$ </span>kubectl create <span class="nt">-f</span> nginx-deployment.yaml <span class="nt">--record</span>
</code></pre></div></div>
<p>通过<code class="language-plaintext highlighter-rouge">kubectl edit</code>指令可进行滚动更新,保存退出,<code class="language-plaintext highlighter-rouge">Kubernetes</code> 就会立刻触发“滚动更新”的过程。你还可以通过 <code class="language-plaintext highlighter-rouge">kubectl rollout status </code>指令查看<code class="language-plaintext highlighter-rouge"> nginx-deployment</code> 的状态变化,将一个集群中正在运行的多个 <code class="language-plaintext highlighter-rouge">Pod</code> 版本,交替地逐一升级的过程,就是“滚动更新”。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl rollout status deployment/nginx-deploymentWaiting <span class="k">for </span>rollout to finish: 2 out of 3 new replicas have been updated...deployment.extensions/nginx-deployment successfully rolled out
</code></pre></div></div>
<h4 id="深入理解statefulset有状态应用">深入理解StatefulSet有状态应用</h4>
<p><code class="language-plaintext highlighter-rouge">StatefulSet</code> 的核心功能,就是通过某种方式记录这些状态,然后在<code class="language-plaintext highlighter-rouge"> Pod</code> 被重新创建时,能够为新 <code class="language-plaintext highlighter-rouge">Pod</code> 恢复这些状态。<code class="language-plaintext highlighter-rouge">StatefulSet</code>这个控制器的主要作用之一,就是使用<code class="language-plaintext highlighter-rouge">Pod </code>模板创建 <code class="language-plaintext highlighter-rouge">Pod</code> 的时候,对它们进行编号,并且按照编号顺序逐一完成创建工作。</p>
<p>当 <code class="language-plaintext highlighter-rouge">StatefulSet</code> 的“控制循环”发现 <code class="language-plaintext highlighter-rouge">Pod </code>的“实际状态”与“期望状态”不一致,需要新建或者删除 <code class="language-plaintext highlighter-rouge">Pod</code> 进行“调谐”的时候,它会严格按照这些 <code class="language-plaintext highlighter-rouge">Pod</code> 编号的顺序,逐一完成这些操作。</p>
<p><code class="language-plaintext highlighter-rouge">DaemonSet</code> 的主要作用,是让你在 <code class="language-plaintext highlighter-rouge">Kubernetes</code> 集群里,运行一个<code class="language-plaintext highlighter-rouge">Daemon Pod</code>。 所以,这个 Pod 有如下三个特征:这个<code class="language-plaintext highlighter-rouge">Pod</code>运行在<code class="language-plaintext highlighter-rouge">Kubernetes</code> 集群里的每一个节点(<code class="language-plaintext highlighter-rouge">Node</code>)上;每个节点上只有一个这样的 <code class="language-plaintext highlighter-rouge">Pod</code> 实例;当有新的节点加入<code class="language-plaintext highlighter-rouge"> Kubernetes</code> 集群后,该 <code class="language-plaintext highlighter-rouge">Pod</code> 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 <code class="language-plaintext highlighter-rouge">Pod</code> 也相应地会被回收掉。</p>
<p>场景比如各种监控组件和日志组件、各种存储插件的 ` Agent ` 组件、各种网络插件的 <code class="language-plaintext highlighter-rouge">Agent </code> 组件都必须在每个节点上部署一个实例。</p>
<p><code class="language-plaintext highlighter-rouge">K8S</code>中<code class="language-plaintext highlighter-rouge">jOb</code>和<code class="language-plaintext highlighter-rouge">cronJob</code>的使用频率不多,<code class="language-plaintext highlighter-rouge">Deployment</code>、<code class="language-plaintext highlighter-rouge">StatefulSet</code>,以及<code class="language-plaintext highlighter-rouge"> DaemonSet</code> 这三个编排概念主要编排“在线业务”,即:<code class="language-plaintext highlighter-rouge">Long Running Task</code>(长作业)。</p>
<p><code class="language-plaintext highlighter-rouge">Operator</code> 的工作原理,实际上是利用了 <code class="language-plaintext highlighter-rouge">Kubernetes</code> 的自定义<code class="language-plaintext highlighter-rouge"> API </code>资源(<code class="language-plaintext highlighter-rouge">CRD</code>),来描述我们想要部署的“有状态应用”;然后在自定义控制器里,根据自定义 <code class="language-plaintext highlighter-rouge">API</code> 对象的变化,来完成具体的部署和运维工作。</p>
kafka client客户端实践及原理剖析
2021-03-02T00:00:00+00:00
https://dongma.github.io/2021/03/02/kafka-client-best-practise
<blockquote>
<p>主要描述<code class="language-plaintext highlighter-rouge">kafka java client</code>的一些实践,以及对<code class="language-plaintext highlighter-rouge">client</code>操作数据的一些原理进行剖析。</p>
</blockquote>
<p><code class="language-plaintext highlighter-rouge">kafka</code>对集群部署环境的一些考虑,<code class="language-plaintext highlighter-rouge">kafka</code> 由 <code class="language-plaintext highlighter-rouge">Scala</code> 语言和 <code class="language-plaintext highlighter-rouge">Java</code> 语言编写而成,编译之后的源代码就是普通的“<code class="language-plaintext highlighter-rouge">.class</code>”文件。本来部署到哪个操作系统应该都是一样的,但是不同操作系统的差异还是给 <code class="language-plaintext highlighter-rouge">Kafka</code> 集群带来了相当大的影响。</p>
<p>主流的操作系统有<code class="language-plaintext highlighter-rouge">3</code>种:<code class="language-plaintext highlighter-rouge">windows</code>、<code class="language-plaintext highlighter-rouge">linux</code>和<code class="language-plaintext highlighter-rouge">macOS</code>,考虑到操作系统与<code class="language-plaintext highlighter-rouge">kafka</code>的适配性,<code class="language-plaintext highlighter-rouge">linux</code>系统显然要比其它两个更加合适部署<code class="language-plaintext highlighter-rouge">kafka</code>,主要在<code class="language-plaintext highlighter-rouge">I/O</code>模式的使用、数据网络传输效率、社区支持度三个方面支持比较好。</p>
<p><code class="language-plaintext highlighter-rouge">linux</code>中的系统调用<code class="language-plaintext highlighter-rouge">select</code>函数属于<code class="language-plaintext highlighter-rouge">I/O</code>多路复用模型,大名鼎鼎的<code class="language-plaintext highlighter-rouge">epoll</code>系统调用则介于<code class="language-plaintext highlighter-rouge">I/O</code> 多路复用、信号驱动<code class="language-plaintext highlighter-rouge">I/O</code>模型。因此在这一点上将<code class="language-plaintext highlighter-rouge">kafka</code> 部署在<code class="language-plaintext highlighter-rouge">Linux</code> 上是有优势的,因为能够获得更高效的 <code class="language-plaintext highlighter-rouge">I/O</code>性能。零拷贝(<code class="language-plaintext highlighter-rouge">Zero Copy</code>)技术,就是当数据在磁盘和网络进行传输时避免昂贵的内核态数据拷贝从而实现快速的数据传输,<code class="language-plaintext highlighter-rouge">Linux</code> 平台实现了这样的零拷贝机制。
<!-- more --></p>
<p>对于磁盘<code class="language-plaintext highlighter-rouge">I/O</code>性能,普通环境使用机械硬盘,不需要搭建<code class="language-plaintext highlighter-rouge">RAID</code>。对于磁盘容量,需根据消息数、留存时间预估磁盘容量,实际使用中建议预留<code class="language-plaintext highlighter-rouge">20%</code>~<code class="language-plaintext highlighter-rouge">30%</code>的磁盘空间。对于网络带宽,需根据实际带宽速度和业务<code class="language-plaintext highlighter-rouge">SLA</code>预估服务器数量,对于千兆网络,建议每台服务器按照<code class="language-plaintext highlighter-rouge">700mps</code>来计算,避免大流量下的丢包问题。</p>
<p><strong>集群配置中一些重要的参数</strong>,<code class="language-plaintext highlighter-rouge">Broker</code>端的一些参数有:</p>
<p>1)<code class="language-plaintext highlighter-rouge">log.dirs</code>指定了<code class="language-plaintext highlighter-rouge">broker</code>需要使用的若干个文件目录路径,而<code class="language-plaintext highlighter-rouge">log.dir</code>结尾没有<code class="language-plaintext highlighter-rouge">s</code>,说明它只能表示单个路径,它是补充上一个参数用的。当挂载多个目录时,其好处在于提升读写性能、能够实现故障转移;</p>
<p>2)<code class="language-plaintext highlighter-rouge">zookeeper</code>的配置,<code class="language-plaintext highlighter-rouge">zookeeper.connect</code>可以指定它的值为<code class="language-plaintext highlighter-rouge">zk1:2181,zk2:2181,zk3:2181</code>。</p>
<p>3)第三组是与<code class="language-plaintext highlighter-rouge">broker</code>连接相关的,<code class="language-plaintext highlighter-rouge">listeners</code>学名叫监听器,其实就是通过<code class="language-plaintext highlighter-rouge">PLAINTEXT://localhost:9092</code>协议连接<code class="language-plaintext highlighter-rouge">kafka</code> 服务的。<code class="language-plaintext highlighter-rouge">advertised.listeners</code>,和 <code class="language-plaintext highlighter-rouge">listeners</code> 相比多了个<code class="language-plaintext highlighter-rouge">advertised</code>,其是在外网连接<code class="language-plaintext highlighter-rouge">kafka</code>的地址。</p>
<p>4)第四组参数是关于 <code class="language-plaintext highlighter-rouge">topic</code> 管理的,<code class="language-plaintext highlighter-rouge">auto.create.topics.enable</code>,是否允许自动创建<code class="language-plaintext highlighter-rouge">topic</code>。<code class="language-plaintext highlighter-rouge">unclean.leader.election.enable</code>:是否允许 <code class="language-plaintext highlighter-rouge">unclean Leader</code> 选举。<code class="language-plaintext highlighter-rouge">auto.leader.rebalance.enable</code>:是否允许定期进行 <code class="language-plaintext highlighter-rouge">Leader</code>选举。</p>
<p>看一些<code class="language-plaintext highlighter-rouge">topic</code>级别的参数,在启动<code class="language-plaintext highlighter-rouge">kafka</code>时设置<code class="language-plaintext highlighter-rouge">jvm</code>的一些参数:</p>
<p>1)<code class="language-plaintext highlighter-rouge">retention.ms</code>:规定了该 <code class="language-plaintext highlighter-rouge">Topic</code> 消息被保存的时长。默认是<code class="language-plaintext highlighter-rouge"> 7</code> 天,即该 <code class="language-plaintext highlighter-rouge">Topic</code> 只保存最近<code class="language-plaintext highlighter-rouge">7</code> 天的消息。一旦设置了这个值,它会覆盖掉 <code class="language-plaintext highlighter-rouge">Broker</code> 端的全局参数值。</p>
<p>2)<code class="language-plaintext highlighter-rouge">retention.bytes</code>:规定了要为该 <code class="language-plaintext highlighter-rouge">topic</code> 预留多大的磁盘空间。当前默认值是<code class="language-plaintext highlighter-rouge">-1</code>,表示可以无限使用磁盘空间。</p>
<p>3)<code class="language-plaintext highlighter-rouge">KAFKA_HEAP_OPTS</code>:指定堆大小,行业经验<code class="language-plaintext highlighter-rouge">kafka</code>默认堆栈大小为<code class="language-plaintext highlighter-rouge">6g</code>,<code class="language-plaintext highlighter-rouge">KAFKA_JVM_PERFORMANCE_OPTS</code>:指定 GC 参数。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$></span> <span class="nb">export </span><span class="nv">KAFKA_HEAP_OPTS</span><span class="o">=</span><span class="nt">--Xms6g</span> <span class="nt">--Xmx6g</span>
<span class="nv">$></span> <span class="nb">export </span><span class="nv">KAFKA_JVM_PERFORMANCE_OPTS</span><span class="o">=</span> <span class="nt">-server</span> <span class="nt">-XX</span>:+UseG1GC <span class="nt">-XX</span>:MaxGCPauseMillis<span class="o">=</span>20 <span class="nt">-XX</span>:InitiatingHeapOccupancyPercent<span class="o">=</span>35 <span class="nt">-XX</span>:+ExplicitGCInvokesConcurrent <span class="nt">-Djava</span>.awt.headless<span class="o">=</span><span class="nb">true</span>
<span class="nv">$></span> bin/kafka-server-start.sh config/server.properties
</code></pre></div></div>
<p><strong>生产者消息分区机制原理剖析</strong>,<code class="language-plaintext highlighter-rouge">Kafka</code> 的消息组织方式实际上是三级结构:主题 - 分区 - 消息。其实分区的作用就是提供负载均衡的能力,或者说对数据进行分区的主要原因,就是为了实现系统的高伸缩性(<code class="language-plaintext highlighter-rouge">scalability</code>)。</p>
<p>所谓分区策略是决定生产者将消息发送到哪个分区的算法,常见的分区策略有轮询策略(<code class="language-plaintext highlighter-rouge">Round-robin</code>)、随机策略(<code class="language-plaintext highlighter-rouge">Randomness</code>)、按消息键保序策略(<code class="language-plaintext highlighter-rouge">Key-ordering</code>)。如下为自定义分区策略,从所有分区中找出哪些<code class="language-plaintext highlighter-rouge">Leader</code> 副本在南方的所有分区,然后随机挑选一个进行消息发送。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">List</span> <span class="n">partitions</span> <span class="o">=</span> <span class="n">cluster</span><span class="o">.</span><span class="na">partitionsForTopic</span><span class="o">(</span><span class="n">topic</span><span class="o">);</span>
<span class="k">return</span> <span class="n">partitions</span><span class="o">.</span><span class="na">stream</span><span class="o">()</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">p</span> <span class="o">-></span><span class="n">isSouth</span><span class="o">(</span><span class="n">p</span><span class="o">.</span><span class="na">leader</span><span class="o">().</span><span class="na">host</span><span class="o">()))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">PartitionInfo:</span><span class="o">:</span><span class="n">partition</span><span class="o">).</span><span class="na">findAny</span><span class="o">().</span><span class="na">get</span><span class="o">();</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">kafka</code>中,压缩可能发生在两个地方:生产者端和<code class="language-plaintext highlighter-rouge">broker</code>端。让<code class="language-plaintext highlighter-rouge">broker</code>端重新压缩消息有<code class="language-plaintext highlighter-rouge">2</code>种例外情况,<code class="language-plaintext highlighter-rouge">broker</code>端指定了和<code class="language-plaintext highlighter-rouge">producer</code>端不同的压缩算法,<code class="language-plaintext highlighter-rouge">broker</code>端发生了消息格式转换。一句话总结压缩和解压缩的话,<code class="language-plaintext highlighter-rouge">producer</code>端压缩、<code class="language-plaintext highlighter-rouge">broker</code>端保持、<code class="language-plaintext highlighter-rouge">consumer</code>端解压缩。</p>
<p>客户端一些高级功能<code class="language-plaintext highlighter-rouge">interceptor</code>,与<code class="language-plaintext highlighter-rouge">spring</code>中的拦截器原理是一样的(<code class="language-plaintext highlighter-rouge">aop</code>),不影响真实业务逻辑调用。生产者要想添加<code class="language-plaintext highlighter-rouge">interceptor</code>,只需继承<code class="language-plaintext highlighter-rouge">ProducerInterceptor<String, String></code>类。</p>
<p>无消息丢失配置如何实现?<code class="language-plaintext highlighter-rouge">producer</code> 永远要使用带有回调通知的发送 API,也就是说不要使用<code class="language-plaintext highlighter-rouge">producer.send(msg)</code>,而要使用 <code class="language-plaintext highlighter-rouge">producer.send(msg, callback)</code>。Kafka 中<code class="language-plaintext highlighter-rouge">consumer</code> 端的消息丢失就是这么一回事。要对抗这种消息丢失,办法很简单:维持先消费消息(阅读),再更新位移(书签)的顺序即可。</p>
<p>设置<code class="language-plaintext highlighter-rouge">acks = all</code>。<code class="language-plaintext highlighter-rouge">acks</code> 是 <code class="language-plaintext highlighter-rouge">Producer </code>的一个参数,代表了你对“已提交”消息的定义。</p>
<p>设置<code class="language-plaintext highlighter-rouge">retries</code> 为一个较大的值。这里的<code class="language-plaintext highlighter-rouge">retries</code> 同样是<code class="language-plaintext highlighter-rouge">Producer</code> 的参数,对应前面提到的<code class="language-plaintext highlighter-rouge">Producer</code>自动重试。</p>
<p>确保消息消费完成再提交。<code class="language-plaintext highlighter-rouge">consumer</code> 端有个参数 <code class="language-plaintext highlighter-rouge">enable.auto.commit</code>,最好把它设置成 <code class="language-plaintext highlighter-rouge">false</code>,并采用手动提交位移的方式。</p>
<p>设置<code class="language-plaintext highlighter-rouge">unclean.leader.election.enable = false</code>、设置<code class="language-plaintext highlighter-rouge">replication.factor >= 3</code>、设置 <code class="language-plaintext highlighter-rouge">min.insync.replicas > 1</code>的配置。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ProducerClient</span> <span class="o">{</span>
<span class="cm">/* kafka用于防止消息丢失的因素: */</span>
<span class="c1">// 1) 维持先消费消息(阅读),再更新位移(书签)的顺序即可。这样就能最大限度地保证消息不丢失。(消费者端 维持先消费, 再提交offset)</span>
<span class="c1">// 2) unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。</span>
<span class="c1">// 如果一个Broker落后原先的 Leader 太多,那么</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Properties</span> <span class="n">kafkaProp</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Properties</span><span class="o">();</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"bootstrap.servers"</span><span class="o">,</span> <span class="s">"localhost:9092"</span><span class="o">);</span>
<span class="c1">// 则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"acks"</span><span class="o">,</span> <span class="s">"all"</span><span class="o">);</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"key.serializer"</span><span class="o">,</span> <span class="s">"org.apache.kafka.common.serialization.StringSerializer"</span><span class="o">);</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"value.serializer"</span><span class="o">,</span> <span class="s">"org.apache.kafka.common.serialization.StringSerializer"</span><span class="o">);</span>
<span class="c1">// 开启kafka的gzip压缩, 向broker发送的每条message都是压缩的</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"compression.type"</span><span class="o">,</span> <span class="s">"gzip"</span><span class="o">);</span>
<span class="c1">// 开启生产者消息的幂等性, 保证底层message消息只会发送一次(用空间换,msg会多传一个字段 用于去重)</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">ENABLE_IDEMPOTENCE_CONFIG</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="c1">// 2. producer生产者启用事务(在kafka 0.11开始的支持)</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">TRANSACTIONAL_ID_CONFIG</span><span class="o">,</span> <span class="s">"kafka-transactional"</span><span class="o">);</span>
<span class="c1">// 设置interceptor用于统计生产者发送消息延时</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">interceptor</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o"><>();</span>
<span class="n">interceptor</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"com.example.kakfa.interceptor.AvgLatencyProducerInterceptor"</span><span class="o">);</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">INTERCEPTOR_CLASSES_CONFIG</span><span class="o">,</span> <span class="n">interceptor</span><span class="o">);</span>
<span class="nc">Producer</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">></span> <span class="n">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">KafkaProducer</span><span class="o"><>(</span><span class="n">kafkaProp</span><span class="o">);</span>
<span class="c1">// 1. send调用时使用回调函数callback, exception 可判断消息是否提交成功,消费者 “位移”类似于我们看书时使用的书签</span>
<span class="n">client</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProducerRecord</span><span class="o"><>(</span><span class="s">""</span><span class="o">,</span> <span class="s">""</span><span class="o">),</span> <span class="o">(</span><span class="n">recordMetadata</span><span class="o">,</span> <span class="n">exception</span><span class="o">)</span> <span class="o">-></span> <span class="o">{</span>
<span class="c1">// RecordMetadata var1, Exception var2</span>
<span class="o">});</span>
<span class="c1">// 2. 在kafka-client客户端中使用transactional事务机制, 用于提交kafka message消息</span>
<span class="n">client</span><span class="o">.</span><span class="na">initTransactions</span><span class="o">();</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">client</span><span class="o">.</span><span class="na">beginTransaction</span><span class="o">();</span>
<span class="n">client</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProducerRecord</span><span class="o"><>(</span><span class="s">"topicA"</span><span class="o">,</span> <span class="s">""</span><span class="o">));</span>
<span class="n">client</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProducerRecord</span><span class="o"><>(</span><span class="s">"topicB"</span><span class="o">,</span> <span class="s">""</span><span class="o">));</span>
<span class="n">client</span><span class="o">.</span><span class="na">commitTransaction</span><span class="o">();</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">ProducerFencedException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="n">client</span><span class="o">.</span><span class="na">abortTransaction</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">kafka</code>社区决定采用<code class="language-plaintext highlighter-rouge">tcp</code>而不是<code class="language-plaintext highlighter-rouge">http</code>,能够利用<code class="language-plaintext highlighter-rouge">TCP</code> 本身提供的一些高级功能,比如多路复用请求以及同时轮询多个连接的能力,目前已知的<code class="language-plaintext highlighter-rouge">HTTP</code> 库在很多编程语言中都略显简陋。</p>
<p>何时创建<code class="language-plaintext highlighter-rouge">TCP</code> 连接?目前我们的结论是这样的,<code class="language-plaintext highlighter-rouge">TCP</code> 连接是在创建 <code class="language-plaintext highlighter-rouge">KafkaProducer</code> 实例时建立的。<code class="language-plaintext highlighter-rouge">TCP</code> 连接还可能在两个地方被创建:一个是在更新元数据后,另一个是在消息发送时。</p>
<p>何时关闭 <code class="language-plaintext highlighter-rouge">TCP </code>连接?<code class="language-plaintext highlighter-rouge">Producer</code> 端关闭<code class="language-plaintext highlighter-rouge">TCP</code>连接的方式有两种:一种是用户主动关闭,一种是 <code class="language-plaintext highlighter-rouge">Kafka</code> 自动关闭。</p>
<p>开启<code class="language-plaintext highlighter-rouge">kafka</code>生产者消息幂等性、producer生产者启用事务需要在<code class="language-plaintext highlighter-rouge">producer</code>的<code class="language-plaintext highlighter-rouge">properties</code>中设置以下配置:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 开启生产者消息的幂等性, 保证底层message消息只会发送一次(用空间换,msg会多传一个字段 用于去重)</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">ENABLE_IDEMPOTENCE_CONFIG</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="c1">// 2. producer生产者启用事务(在kafka 0.11开始的支持)</span>
<span class="n">kafkaProp</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">TRANSACTIONAL_ID_CONFIG</span><span class="o">,</span> <span class="s">"kafka-transactional"</span><span class="o">);</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Consumer Group</code> 是 <code class="language-plaintext highlighter-rouge">Kafka</code> 提供的可扩展且具有容错性的消费者机制。既然是一个组,那么组内必然可以有多个消费者或消费者实例<code class="language-plaintext highlighter-rouge">(Consumer Instance)</code>,它们共享一个公共的 <code class="language-plaintext highlighter-rouge">ID</code>,这个 <code class="language-plaintext highlighter-rouge">ID</code> 被称为 <code class="language-plaintext highlighter-rouge">Group ID</code>。组内的所有消费者协调在一起来消费订阅主题<code class="language-plaintext highlighter-rouge">(Subscribed Topics)</code>的所有分区<code class="language-plaintext highlighter-rouge">(Partition)</code>。</p>
<p>Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 Consumer 如何达成一致,来分配订阅 Topic 的每个分区。比如某个 Group 下有 20 个 Consumer 实例,它订阅了一个具有 100 个分区的 Topic。正常情况下,Kafka 平均会为每个 Consumer 分配 5 个分区。这个分配的过程就叫 Rebalance。</p>
<p>那么 <code class="language-plaintext highlighter-rouge">Consumer Group</code> 何时进行 <code class="language-plaintext highlighter-rouge">Rebalance </code>呢?<code class="language-plaintext highlighter-rouge">Rebalance</code> 的触发条件有 <code class="language-plaintext highlighter-rouge">3 </code>个。</p>
<p>1)组成员数发生变更。比如有新的<code class="language-plaintext highlighter-rouge"> Consumer </code>实例加入组或者离开组,抑或是有 <code class="language-plaintext highlighter-rouge">Consumer </code>实例崩溃被“踢出”组。</p>
<p>2)订阅主题数发生变更。<code class="language-plaintext highlighter-rouge">Consumer Group</code> 可以使用正则表达式的方式订阅主题,比如 <code class="language-plaintext highlighter-rouge">consumer.subscribe(Pattern.compile("t.*c")) </code>就表明该 <code class="language-plaintext highlighter-rouge">Group</code> 订阅所有以字母<code class="language-plaintext highlighter-rouge"> t </code>开头、字母 <code class="language-plaintext highlighter-rouge">c </code>结尾的主题。在 <code class="language-plaintext highlighter-rouge">Consumer Group </code>的运行过程中,你新创建了一个满足这样条件的主题,那么该<code class="language-plaintext highlighter-rouge"> Group</code> 就会发生<code class="language-plaintext highlighter-rouge"> Rebalance</code>。</p>
<p>3)订阅主题的分区数发生变更。<code class="language-plaintext highlighter-rouge">Kafka</code> 当前只能允许增加一个主题的分区数。当分区数增加时,就会触发订阅该主题的所有 <code class="language-plaintext highlighter-rouge">Group</code> 开启 <code class="language-plaintext highlighter-rouge">Rebalance</code>。</p>
分析spark在yarn-client和yarn-cluster模式下启动
2021-02-23T00:00:00+00:00
https://dongma.github.io/2021/02/23/spark-yarn-mode
<blockquote>
<p>文章分析<code class="language-plaintext highlighter-rouge">spark</code>在<code class="language-plaintext highlighter-rouge">yarn-client</code>、<code class="language-plaintext highlighter-rouge">yarn-cluster</code>模式下启动的流程,<code class="language-plaintext highlighter-rouge">yarn</code>是<code class="language-plaintext highlighter-rouge">apache</code>开源的一个资源管理的组件。<code class="language-plaintext highlighter-rouge">JobTracker</code>在<code class="language-plaintext highlighter-rouge">yarn</code>中大致分为了三块:一部分是<code class="language-plaintext highlighter-rouge">ResourceManager</code>,负责<code class="language-plaintext highlighter-rouge">Scheduler</code>及<code class="language-plaintext highlighter-rouge">ApplicationsManager</code>;一部分是<code class="language-plaintext highlighter-rouge">ApplicationMaster</code>,负责<code class="language-plaintext highlighter-rouge">job</code>生命周期的管理;最后一部分是<code class="language-plaintext highlighter-rouge">JobHistoryServer</code>,负责日志的展示;</p>
</blockquote>
<p>先看一个<code class="language-plaintext highlighter-rouge">spark</code>官网上通过<code class="language-plaintext highlighter-rouge">yarn</code>提交用户应用程序的<code class="language-plaintext highlighter-rouge">spark-submit</code>脚本,从该脚本开始分析在<code class="language-plaintext highlighter-rouge">yarn</code>环境下执行的流程。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./bin/spark-submit <span class="se">\</span>
<span class="nt">--class</span> org.apache.spark.examples.SparkPi <span class="se">\</span>
<span class="nt">--master</span> yarn <span class="se">\</span>
<span class="nt">--deploy-mode</span> cluster <span class="se">\ </span> <span class="c"># can be client for client mode</span>
<span class="nt">--executor-memory</span> 20G <span class="se">\</span>
<span class="nt">--num-executors</span> 50 <span class="se">\</span>
/path/to/examples.jar <span class="se">\</span>
1000
</code></pre></div></div>
<p>在分析源码前需要在父<code class="language-plaintext highlighter-rouge">pom.xml</code>中引入<code class="language-plaintext highlighter-rouge">yarn</code>资源代码模块,使得其<code class="language-plaintext highlighter-rouge">class</code>文件加载到<code class="language-plaintext highlighter-rouge">classpath</code>中。
<!-- more --></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- See additional modules enabled by profiles below --></span>
<span class="nt"><module></span>resource-managers/yarn<span class="nt"></module></span>
</code></pre></div></div>
<p>与<code class="language-plaintext highlighter-rouge">standalone</code>模式应用启动一样,<code class="language-plaintext highlighter-rouge">SparkSubmit#runMain(SparkSubmitArguments, Boolean)</code>是应用程序的入口。由于是在<code class="language-plaintext highlighter-rouge">yarn</code>环境下启动,在前期准备<code class="language-plaintext highlighter-rouge">submit</code>环境时会有差异,差异点在<code class="language-plaintext highlighter-rouge">prepareSubmitEnvironment(SparkSubmitArguments, Option[HadoopConfiguration])</code>方法,在方法中会依<code class="language-plaintext highlighter-rouge">args.master</code>、<code class="language-plaintext highlighter-rouge">args.deployMode</code>进行模式匹配,当<code class="language-plaintext highlighter-rouge">master</code>为<code class="language-plaintext highlighter-rouge">yarn</code>时,会将<code class="language-plaintext highlighter-rouge">childMainClass</code>设置为<code class="language-plaintext highlighter-rouge">org.apache.spark.deploy.yarn.YarnClusterApplication</code>作为资源调度的启动类。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span><span class="o">[</span><span class="kt">deploy</span><span class="o">]</span> <span class="k">def</span> <span class="nf">prepareSubmitEnvironment</span><span class="o">(</span>
<span class="n">args</span><span class="k">:</span> <span class="kt">SparkSubmitArguments</span><span class="o">,</span>
<span class="n">conf</span><span class="k">:</span> <span class="kt">Option</span><span class="o">[</span><span class="kt">HadoopConfiguration</span><span class="o">]</span> <span class="k">=</span> <span class="nc">None</span><span class="o">)</span>
<span class="k">:</span> <span class="o">(</span><span class="kt">Seq</span><span class="o">[</span><span class="kt">String</span><span class="o">],</span> <span class="nc">Seq</span><span class="o">[</span><span class="kt">String</span><span class="o">],</span> <span class="nc">SparkConf</span><span class="o">,</span> <span class="nc">String</span><span class="o">)</span> <span class="k">=</span> <span class="o">{</span>
<span class="c1">// Set the cluster manager</span>
<span class="k">val</span> <span class="nv">clusterManager</span><span class="k">:</span> <span class="kt">Int</span> <span class="o">=</span> <span class="nv">args</span><span class="o">.</span><span class="py">master</span> <span class="k">match</span> <span class="o">{</span>
<span class="k">case</span> <span class="s">"yarn"</span> <span class="k">=></span> <span class="nc">YARN</span>
<span class="k">case</span> <span class="s">"yarn-client"</span> <span class="o">|</span> <span class="s">"yarn-cluster"</span> <span class="k">=></span>
<span class="nf">logWarning</span><span class="o">(</span><span class="n">s</span><span class="s">"Master ${args.master} is deprecated since 2.0."</span> <span class="o">+</span>
<span class="s">" Please use master \"yarn\" with specified deploy mode instead."</span><span class="o">)</span>
<span class="nc">YARN</span>
<span class="o">}</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">deployMode</span> <span class="o">==</span> <span class="nc">CLIENT</span><span class="o">)</span> <span class="o">{</span>
<span class="cm">/* 在client模式下 用户程序直接在submit内通过反射机制执行,此时用户自己打的jar和--jars指定的jar都会被加载到classpath中 */</span>
<span class="n">childMainClass</span> <span class="k">=</span> <span class="nv">args</span><span class="o">.</span><span class="py">mainClass</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">localPrimaryResource</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="nf">isUserJar</span><span class="o">(</span><span class="n">localPrimaryResource</span><span class="o">))</span> <span class="o">{</span>
<span class="n">childClasspath</span> <span class="o">+=</span> <span class="n">localPrimaryResource</span>
<span class="o">}</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">localJars</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">childClasspath</span> <span class="o">++=</span> <span class="nv">localJars</span><span class="o">.</span><span class="py">split</span><span class="o">(</span><span class="s">","</span><span class="o">)</span> <span class="o">}</span>
<span class="o">}</span>
<span class="c1">// In yarn-cluster mode, use yarn.Client as a wrapper around the user class</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">isYarnCluster</span><span class="o">)</span> <span class="o">{</span>
<span class="cm">/* YARN_CLUSTER_SUBMIT_CLASS在cluster模式下为org.apache.spark.deploy.yarn.YarnClusterApplication */</span>
<span class="n">childMainClass</span> <span class="k">=</span> <span class="nc">YARN_CLUSTER_SUBMIT_CLASS</span>
<span class="o">}</span>
<span class="o">(</span><span class="n">childArgs</span><span class="o">,</span> <span class="n">childClasspath</span><span class="o">,</span> <span class="n">sparkConf</span><span class="o">,</span> <span class="n">childMainClass</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">submit()</code>需要的环境准备好之后,通过<code class="language-plaintext highlighter-rouge">mainClass</code>构建<code class="language-plaintext highlighter-rouge">spark</code>应用,由于目前分析在<code class="language-plaintext highlighter-rouge">yarn client</code>模式下的启动,<code class="language-plaintext highlighter-rouge">mainClass</code>并不是<code class="language-plaintext highlighter-rouge">SparkApplication</code>的实例。因而,<code class="language-plaintext highlighter-rouge">app</code>类型为<code class="language-plaintext highlighter-rouge">JavaMainApplication</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">val</span> <span class="nv">app</span><span class="k">:</span> <span class="kt">SparkApplication</span> <span class="o">=</span> <span class="nf">if</span> <span class="o">(</span><span class="n">classOf</span><span class="o">[</span><span class="kt">SparkApplication</span><span class="o">].</span><span class="py">isAssignableFrom</span><span class="o">(</span><span class="n">mainClass</span><span class="o">))</span> <span class="o">{</span>
<span class="nv">mainClass</span><span class="o">.</span><span class="py">newInstance</span><span class="o">().</span><span class="py">asInstanceOf</span><span class="o">[</span><span class="kt">SparkApplication</span><span class="o">]</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="c1">// SPARK-4170</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">classOf</span><span class="o">[</span><span class="kt">scala.App</span><span class="o">].</span><span class="py">isAssignableFrom</span><span class="o">(</span><span class="n">mainClass</span><span class="o">))</span> <span class="o">{</span>
<span class="nf">logWarning</span><span class="o">(</span><span class="s">"Subclasses of scala.App may not work correctly. Use a main() method instead."</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">new</span> <span class="nc">JavaMainApplication</span><span class="o">(</span><span class="n">mainClass</span><span class="o">)</span>
<span class="o">}</span>
<span class="cm">/* standalone模式在 SparkSubmit#prepareSubmitEnvironment(args)中将childMainClass设置为RestSubmissionClient */</span>
<span class="nv">app</span><span class="o">.</span><span class="py">start</span><span class="o">(</span><span class="nv">childArgs</span><span class="o">.</span><span class="py">toArray</span><span class="o">,</span> <span class="n">sparkConf</span><span class="o">)</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">start()</code>方法中会通过反射获取得到<code class="language-plaintext highlighter-rouge">main</code>方法,然后进行调用执行用户<code class="language-plaintext highlighter-rouge">jar</code>包中的代码。进入用户程序(<code class="language-plaintext highlighter-rouge">main</code>方法)之后,存在两个重要的类<code class="language-plaintext highlighter-rouge">SparkConf</code>和<code class="language-plaintext highlighter-rouge">SparkContext</code>,根据<code class="language-plaintext highlighter-rouge">config</code>配置信息实例化<code class="language-plaintext highlighter-rouge">context</code>上下文。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">start</span><span class="o">(</span><span class="n">args</span><span class="k">:</span> <span class="kt">Array</span><span class="o">[</span><span class="kt">String</span><span class="o">],</span> <span class="n">conf</span><span class="k">:</span> <span class="kt">SparkConf</span><span class="o">)</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">mainMethod</span> <span class="k">=</span> <span class="nv">klass</span><span class="o">.</span><span class="py">getMethod</span><span class="o">(</span><span class="s">"main"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Array</span><span class="o">[</span><span class="kt">String</span><span class="o">](</span><span class="mi">0</span><span class="o">).</span><span class="py">getClass</span><span class="o">)</span>
<span class="nf">if</span> <span class="o">(!</span><span class="nv">Modifier</span><span class="o">.</span><span class="py">isStatic</span><span class="o">(</span><span class="nv">mainMethod</span><span class="o">.</span><span class="py">getModifiers</span><span class="o">))</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nc">IllegalStateException</span><span class="o">(</span><span class="s">"The main method in the given main class must be static"</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">val</span> <span class="nv">sysProps</span> <span class="k">=</span> <span class="nv">conf</span><span class="o">.</span><span class="py">getAll</span><span class="o">.</span><span class="py">toMap</span>
<span class="nv">sysProps</span><span class="o">.</span><span class="py">foreach</span> <span class="o">{</span> <span class="nf">case</span> <span class="o">(</span><span class="n">k</span><span class="o">,</span> <span class="n">v</span><span class="o">)</span> <span class="k">=></span>
<span class="nv">sys</span><span class="o">.</span><span class="py">props</span><span class="o">(</span><span class="n">k</span><span class="o">)</span> <span class="k">=</span> <span class="n">v</span>
<span class="o">}</span>
<span class="nv">mainMethod</span><span class="o">.</span><span class="py">invoke</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="n">args</span><span class="o">)</span>
<span class="o">}</span>
<span class="cm">/* spark application中使用sparkConf和sparkContext加载环境相关配置 */</span>
<span class="k">val</span> <span class="nv">config</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">SparkConf</span><span class="o">().</span><span class="py">setAppName</span><span class="o">(</span><span class="s">"spark-app"</span><span class="o">)</span>
<span class="o">.</span><span class="py">set</span><span class="o">(</span><span class="s">"spark.app.id"</span><span class="o">,</span> <span class="s">"spark-mongo-connector"</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">sparkContext</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">SparkContext</span><span class="o">(</span><span class="n">config</span><span class="o">)</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">SparkContext#createTaskScheduler(SparkContext, String, String)</code>方法中会根据<code class="language-plaintext highlighter-rouge">master</code>确定<code class="language-plaintext highlighter-rouge">scheduler</code>和<code class="language-plaintext highlighter-rouge">backend</code>。由于<code class="language-plaintext highlighter-rouge">master</code>为<code class="language-plaintext highlighter-rouge">yarn</code>,在<code class="language-plaintext highlighter-rouge">getClusterManager(String)</code>中确定<code class="language-plaintext highlighter-rouge">cm</code>的类型为<code class="language-plaintext highlighter-rouge">YarnClusterManager</code>。在<code class="language-plaintext highlighter-rouge">yarn-client</code>模式下调用<code class="language-plaintext highlighter-rouge">createTaskScheduler()</code>和<code class="language-plaintext highlighter-rouge">createSchedulerBackend()</code>通过<code class="language-plaintext highlighter-rouge">masterUrl</code>和<code class="language-plaintext highlighter-rouge">deployMode</code>可得 <code class="language-plaintext highlighter-rouge">scheduler</code>为<code class="language-plaintext highlighter-rouge">YarnScheduler</code>、<code class="language-plaintext highlighter-rouge">backend</code>为<code class="language-plaintext highlighter-rouge">YarnClientSchedulerBackend</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* 当masterUrl为外部资源时 (Yarn、Mesos、K8s),走此处的逻辑: (yarn)cluster模式走YarnClusterScheduler、
(yarn)client走YarnScheduler用于资源调度 */</span>
<span class="k">case</span> <span class="n">masterUrl</span> <span class="k">=></span>
<span class="k">val</span> <span class="nv">cm</span> <span class="k">=</span> <span class="nf">getClusterManager</span><span class="o">(</span><span class="n">masterUrl</span><span class="o">)</span> <span class="k">match</span> <span class="o">{</span>
<span class="k">case</span> <span class="nc">Some</span><span class="o">(</span><span class="n">clusterMgr</span><span class="o">)</span> <span class="k">=></span> <span class="n">clusterMgr</span>
<span class="k">case</span> <span class="nc">None</span> <span class="k">=></span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">SparkException</span><span class="o">(</span><span class="s">"Could not parse Master URL: '"</span> <span class="o">+</span> <span class="n">master</span> <span class="o">+</span> <span class="s">"'"</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">val</span> <span class="nv">scheduler</span> <span class="k">=</span> <span class="nv">cm</span><span class="o">.</span><span class="py">createTaskScheduler</span><span class="o">(</span><span class="n">sc</span><span class="o">,</span> <span class="n">masterUrl</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">backend</span> <span class="k">=</span> <span class="nv">cm</span><span class="o">.</span><span class="py">createSchedulerBackend</span><span class="o">(</span><span class="n">sc</span><span class="o">,</span> <span class="n">masterUrl</span><span class="o">,</span> <span class="n">scheduler</span><span class="o">)</span>
<span class="nv">cm</span><span class="o">.</span><span class="py">initialize</span><span class="o">(</span><span class="n">scheduler</span><span class="o">,</span> <span class="n">backend</span><span class="o">)</span>
</code></pre></div></div>
<p>进入<code class="language-plaintext highlighter-rouge">YarnClientSchedulerBackend#start()</code>方法,创建<code class="language-plaintext highlighter-rouge">client</code>对象去提交任务,然后调用<code class="language-plaintext highlighter-rouge">client.submitApplication()</code>使用<code class="language-plaintext highlighter-rouge">AM</code>向<code class="language-plaintext highlighter-rouge">ResourceManager</code>申请资源。在<code class="language-plaintext highlighter-rouge">super.start()</code>中会启动<code class="language-plaintext highlighter-rouge">CoarseGrainedSchedulerBackend</code>,等待<code class="language-plaintext highlighter-rouge">app</code>的启动成功。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">start</span><span class="o">()</span> <span class="o">{</span>
<span class="cm">/* 动态申请资源的时候才会调用 SchedulerBackendUtils#getInitialTargetExecutorNumber */</span>
<span class="n">totalExpectedExecutors</span> <span class="k">=</span> <span class="nv">SchedulerBackendUtils</span><span class="o">.</span><span class="py">getInitialTargetExecutorNumber</span><span class="o">(</span><span class="n">conf</span><span class="o">)</span>
<span class="n">client</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">Client</span><span class="o">(</span><span class="n">args</span><span class="o">,</span> <span class="n">conf</span><span class="o">)</span>
<span class="cm">/* 将Application提交之后 # 可看ApplicationMaster#main()的启动 */</span>
<span class="nf">bindToYarn</span><span class="o">(</span><span class="nv">client</span><span class="o">.</span><span class="py">submitApplication</span><span class="o">(),</span> <span class="nc">None</span><span class="o">)</span>
<span class="c1">// SPARK-8687: Ensure all necessary properties have already been set before</span>
<span class="c1">// we initialize our driver scheduler backend, which serves these properties</span>
<span class="c1">// to the executors</span>
<span class="cm">/* 调用YarnSchedulerBackend的父类CoarseGrainedSchedulerBackend#start()方法,在start()方法里实现自己 */</span>
<span class="nv">super</span><span class="o">.</span><span class="py">start</span><span class="o">()</span>
<span class="nf">waitForApplication</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>进一步看<code class="language-plaintext highlighter-rouge">client.submitApplication()</code>提交应用给<code class="language-plaintext highlighter-rouge">AppMaster</code>前,如何初始化<code class="language-plaintext highlighter-rouge">ContainerContext</code>运行环境、<code class="language-plaintext highlighter-rouge">java opts</code>和运行<code class="language-plaintext highlighter-rouge">AM</code>的指令,进入<code class="language-plaintext highlighter-rouge">createContainerLaunchContext()</code>方法,<code class="language-plaintext highlighter-rouge">client</code>模式下<code class="language-plaintext highlighter-rouge">amClass</code>为<code class="language-plaintext highlighter-rouge">org.apache.spark.deploy.yarn.ExecutorLauncher</code>。在<code class="language-plaintext highlighter-rouge">yarn client</code>模式下,都是有<code class="language-plaintext highlighter-rouge">appMaster</code>向<code class="language-plaintext highlighter-rouge">resourceManager</code>申请<code class="language-plaintext highlighter-rouge">--num-executor NUM</code>参数指定的数目。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Set up a ContainerLaunchContext to launch our ApplicationMaster container.
* This sets up the launch environment, java options, and the command for launching the AM.
*/</span>
<span class="k">private</span> <span class="k">def</span> <span class="nf">createContainerLaunchContext</span><span class="o">(</span><span class="n">newAppResponse</span><span class="k">:</span> <span class="kt">GetNewApplicationResponse</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 设置环境变量及spark-java-opts</span>
<span class="k">val</span> <span class="nv">launchEnv</span> <span class="k">=</span> <span class="nf">setupLaunchEnv</span><span class="o">(</span><span class="n">appStagingDirPath</span><span class="o">,</span> <span class="n">pySparkArchives</span><span class="o">)</span>
<span class="cm">/*
* 这个函数的主要作用是将用户自己打的jar包(--jars指定的jar发送到分布式缓存中去),并设置了spark.yarn.user.jar
* 和spark.yarn.secondary.jars这两个参数, 然后这两个参数会被封装程 --user-class-path 传递给
* executor使用
*/</span>
<span class="k">val</span> <span class="nv">localResources</span> <span class="k">=</span> <span class="nf">prepareLocalResources</span><span class="o">(</span><span class="n">appStagingDirPath</span><span class="o">,</span> <span class="n">pySparkArchives</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">amContainer</span> <span class="k">=</span> <span class="nv">Records</span><span class="o">.</span><span class="py">newRecord</span><span class="o">(</span><span class="n">classOf</span><span class="o">[</span><span class="kt">ContainerLaunchContext</span><span class="o">])</span>
<span class="nv">amContainer</span><span class="o">.</span><span class="py">setLocalResources</span><span class="o">(</span><span class="nv">localResources</span><span class="o">.</span><span class="py">asJava</span><span class="o">)</span>
<span class="nv">amContainer</span><span class="o">.</span><span class="py">setEnvironment</span><span class="o">(</span><span class="nv">launchEnv</span><span class="o">.</span><span class="py">asJava</span><span class="o">)</span>
<span class="c1">// Add Xmx for AM memory</span>
<span class="n">javaOpts</span> <span class="o">+=</span> <span class="s">"-Xmx"</span> <span class="o">+</span> <span class="n">amMemory</span> <span class="o">+</span> <span class="s">"m"</span>
<span class="k">val</span> <span class="nv">tmpDir</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">Path</span><span class="o">(</span><span class="nv">Environment</span><span class="o">.</span><span class="py">PWD</span><span class="o">.</span><span class="py">$$</span><span class="o">(),</span> <span class="nv">YarnConfiguration</span><span class="o">.</span><span class="py">DEFAULT_CONTAINER_TEMP_DIR</span><span class="o">)</span>
<span class="n">javaOpts</span> <span class="o">+=</span> <span class="s">"-Djava.io.tmpdir="</span> <span class="o">+</span> <span class="n">tmpDir</span>
<span class="cm">/* 判断是否在cluster集群环境来确定AMclass, client模式下为ExecutorLauncher, 通过AMclass及一些参数构建command 进而构建amContainer */</span>
<span class="k">val</span> <span class="nv">amClass</span> <span class="k">=</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">isClusterMode</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">Utils</span><span class="o">.</span><span class="py">classForName</span><span class="o">(</span><span class="s">"org.apache.spark.deploy.yarn.ApplicationMaster"</span><span class="o">).</span><span class="py">getName</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="nv">Utils</span><span class="o">.</span><span class="py">classForName</span><span class="o">(</span><span class="s">"org.apache.spark.deploy.yarn.ExecutorLauncher"</span><span class="o">).</span><span class="py">getName</span>
<span class="o">}</span>
<span class="n">amContainer</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">super.start()</code>需要重点看一下<code class="language-plaintext highlighter-rouge">YarnSchedulerBackend</code>的父类<code class="language-plaintext highlighter-rouge">CoarseGrainedSchedulerBackend</code>的<code class="language-plaintext highlighter-rouge">start()</code>方法,方法体内创建了一个<code class="language-plaintext highlighter-rouge">driverEndpoint</code>的<code class="language-plaintext highlighter-rouge">RPC</code>客户端。在<code class="language-plaintext highlighter-rouge">YarnSchedulerBackend</code>类中覆盖了<code class="language-plaintext highlighter-rouge">createDriverEndpointRef()</code>方法,用子类<code class="language-plaintext highlighter-rouge">YarnDriverEndpoint</code>替代<code class="language-plaintext highlighter-rouge">DriverEndpoint</code>并重写了其<code class="language-plaintext highlighter-rouge">onDisconnected()</code>方法(是由于协议的不同)。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* YarnSchedulerBackend启动时实例化,负责根ApplicationMaster进行通信 */</span>
<span class="k">private</span> <span class="k">val</span> <span class="nv">yarnSchedulerEndpoint</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">YarnSchedulerEndpoint</span><span class="o">(</span><span class="n">rpcEnv</span><span class="o">)</span>
<span class="k">override</span> <span class="k">def</span> <span class="nf">start</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// TODO (prashant) send conf instead of properties</span>
<span class="n">driverEndpoint</span> <span class="k">=</span> <span class="nf">createDriverEndpointRef</span><span class="o">(</span><span class="n">properties</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">yarn-client</code>代码分析完之后,进入<code class="language-plaintext highlighter-rouge">ApplicationMaster#main(Array[String])</code>,在上文<code class="language-plaintext highlighter-rouge">client#createContainerLaunchContext()</code>时,指定<code class="language-plaintext highlighter-rouge">amClass</code>为<code class="language-plaintext highlighter-rouge">org.apache.spark.deploy.yarn.ExecutorLauncher</code>(<code class="language-plaintext highlighter-rouge">main</code>方法中封装了<code class="language-plaintext highlighter-rouge">ApplicationMaster</code>),最终调用<code class="language-plaintext highlighter-rouge">runExecutorLauncher()</code>运行<code class="language-plaintext highlighter-rouge">executor</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">def</span> <span class="nf">runExecutorLauncher</span><span class="o">()</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">hostname</span> <span class="k">=</span> <span class="nv">Utils</span><span class="o">.</span><span class="py">localHostName</span>
<span class="k">val</span> <span class="nv">amCores</span> <span class="k">=</span> <span class="nv">sparkConf</span><span class="o">.</span><span class="py">get</span><span class="o">(</span><span class="nc">AM_CORES</span><span class="o">)</span>
<span class="n">rpcEnv</span> <span class="k">=</span> <span class="nv">RpcEnv</span><span class="o">.</span><span class="py">create</span><span class="o">(</span><span class="s">"sparkYarnAM"</span><span class="o">,</span> <span class="n">hostname</span><span class="o">,</span> <span class="n">hostname</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">,</span> <span class="n">sparkConf</span><span class="o">,</span> <span class="n">securityMgr</span><span class="o">,</span>
<span class="n">amCores</span><span class="o">,</span> <span class="kc">true</span><span class="o">)</span>
<span class="c1">// The client-mode AM doesn't listen for incoming connections, so report an invalid port.</span>
<span class="nf">registerAM</span><span class="o">(</span><span class="n">hostname</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">,</span> <span class="n">sparkConf</span><span class="o">,</span> <span class="nv">sparkConf</span><span class="o">.</span><span class="py">getOption</span><span class="o">(</span><span class="s">"spark.driver.appUIAddress"</span><span class="o">))</span>
<span class="c1">// The driver should be up and listening, so unlike cluster mode, just try to connect to it</span>
<span class="c1">// with no waiting or retrying.</span>
<span class="nf">val</span> <span class="o">(</span><span class="n">driverHost</span><span class="o">,</span> <span class="n">driverPort</span><span class="o">)</span> <span class="k">=</span> <span class="nv">Utils</span><span class="o">.</span><span class="py">parseHostPort</span><span class="o">(</span><span class="nv">args</span><span class="o">.</span><span class="py">userArgs</span><span class="o">(</span><span class="mi">0</span><span class="o">))</span>
<span class="k">val</span> <span class="nv">driverRef</span> <span class="k">=</span> <span class="nv">rpcEnv</span><span class="o">.</span><span class="py">setupEndpointRef</span><span class="o">(</span>
<span class="nc">RpcAddress</span><span class="o">(</span><span class="n">driverHost</span><span class="o">,</span> <span class="n">driverPort</span><span class="o">),</span>
<span class="nv">YarnSchedulerBackend</span><span class="o">.</span><span class="py">ENDPOINT_NAME</span><span class="o">)</span>
<span class="nf">addAmIpFilter</span><span class="o">(</span><span class="nc">Some</span><span class="o">(</span><span class="n">driverRef</span><span class="o">))</span>
<span class="cm">/* 向resourceManager申请根启动--num-executor相同的资源 */</span>
<span class="nf">createAllocator</span><span class="o">(</span><span class="n">driverRef</span><span class="o">,</span> <span class="n">sparkConf</span><span class="o">)</span>
<span class="c1">// In client mode the actor will stop the reporter thread.</span>
<span class="nv">reporterThread</span><span class="o">.</span><span class="py">join</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">appMaster#createAllocator()</code>会进入到<code class="language-plaintext highlighter-rouge">allocator#allocateResources()</code>申请资源,接着进入<code class="language-plaintext highlighter-rouge">handleAllocatedContainers(Seq[Container])</code>方法。在<code class="language-plaintext highlighter-rouge">runAllocatedContainers()</code>中在已经申请到的<code class="language-plaintext highlighter-rouge">container</code>中运行<code class="language-plaintext highlighter-rouge">executor</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Launches executors in the allocated containers.
*/</span>
<span class="k">private</span> <span class="k">def</span> <span class="nf">runAllocatedContainers</span><span class="o">(</span><span class="n">containersToUse</span><span class="k">:</span> <span class="kt">ArrayBuffer</span><span class="o">[</span><span class="kt">Container</span><span class="o">])</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">new</span> <span class="nc">ExecutorRunnable</span><span class="o">(</span>
<span class="nc">Some</span><span class="o">(</span><span class="n">container</span><span class="o">),</span> <span class="n">conf</span><span class="o">,</span> <span class="n">sparkConf</span><span class="o">,</span> <span class="n">driverUrl</span><span class="o">,</span> <span class="n">executorId</span><span class="o">,</span> <span class="n">executorHostname</span><span class="o">,</span><span class="n">executorMemory</span><span class="o">,</span>
<span class="n">executorCores</span><span class="o">,</span> <span class="nv">appAttemptId</span><span class="o">.</span><span class="py">getApplicationId</span><span class="o">.</span><span class="py">toString</span><span class="o">,</span> <span class="n">securityMgr</span><span class="o">,</span> <span class="n">localResources</span>
<span class="o">).</span><span class="py">run</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">ExecutorRunnable#startContainer()</code>中会设置本地相关环境变量,然后<code class="language-plaintext highlighter-rouge">nmClient</code>会启动<code class="language-plaintext highlighter-rouge">container</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">startContainer</span><span class="o">()</span><span class="k">:</span> <span class="kt">java.util.Map</span><span class="o">[</span><span class="kt">String</span>, <span class="kt">ByteBuffer</span><span class="o">]</span> <span class="k">=</span> <span class="o">{</span>
<span class="cm">/* 此处设置spark.executor.extraClassPath为系统环境变量 */</span>
<span class="nv">ctx</span><span class="o">.</span><span class="py">setLocalResources</span><span class="o">(</span><span class="nv">localResources</span><span class="o">.</span><span class="py">asJava</span><span class="o">)</span>
<span class="c1">// Send the start request to the ContainerManager</span>
<span class="k">try</span> <span class="o">{</span>
<span class="nv">nmClient</span><span class="o">.</span><span class="py">startContainer</span><span class="o">(</span><span class="nv">container</span><span class="o">.</span><span class="py">get</span><span class="o">,</span> <span class="n">ctx</span><span class="o">)</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">{</span>
<span class="k">case</span> <span class="n">ex</span><span class="k">:</span> <span class="kt">Exception</span> <span class="o">=></span>
<span class="k">throw</span> <span class="k">new</span> <span class="nc">SparkException</span><span class="o">(</span><span class="n">s</span><span class="s">"Exception while starting container ${container.get.getId}"</span> <span class="o">+</span>
<span class="n">s</span><span class="s">" on host $hostname"</span><span class="o">,</span> <span class="n">ex</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">CoarseGrainedExecutorBackend#main(Array[String])</code>启动时会执行<code class="language-plaintext highlighter-rouge">run(driverUrl, executorId, hostname, cores, appId, workerUrl, userClassPath)</code>的方法。先创建<code class="language-plaintext highlighter-rouge">env</code>然后根据<code class="language-plaintext highlighter-rouge">env</code>使用<code class="language-plaintext highlighter-rouge">CoarseGrainedExecutorBackend</code>作为<code class="language-plaintext highlighter-rouge">executor</code>创建<code class="language-plaintext highlighter-rouge">rpc</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* 创建env主要用与Rpc提交相关的请求 */</span>
<span class="k">val</span> <span class="nv">env</span> <span class="k">=</span> <span class="nv">SparkEnv</span><span class="o">.</span><span class="py">createExecutorEnv</span><span class="o">(</span>
<span class="n">driverConf</span><span class="o">,</span> <span class="n">executorId</span><span class="o">,</span> <span class="n">hostname</span><span class="o">,</span> <span class="n">cores</span><span class="o">,</span> <span class="nv">cfg</span><span class="o">.</span><span class="py">ioEncryptionKey</span><span class="o">,</span> <span class="n">isLocal</span> <span class="k">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nv">env</span><span class="o">.</span><span class="py">rpcEnv</span><span class="o">.</span><span class="py">setupEndpoint</span><span class="o">(</span><span class="s">"Executor"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">CoarseGrainedExecutorBackend</span><span class="o">(</span>
<span class="nv">env</span><span class="o">.</span><span class="py">rpcEnv</span><span class="o">,</span> <span class="n">driverUrl</span><span class="o">,</span> <span class="n">executorId</span><span class="o">,</span> <span class="n">hostname</span><span class="o">,</span> <span class="n">cores</span><span class="o">,</span> <span class="n">userClassPath</span><span class="o">,</span> <span class="n">env</span><span class="o">))</span>
<span class="nv">workerUrl</span><span class="o">.</span><span class="py">foreach</span> <span class="o">{</span> <span class="n">url</span> <span class="k">=></span>
<span class="nv">env</span><span class="o">.</span><span class="py">rpcEnv</span><span class="o">.</span><span class="py">setupEndpoint</span><span class="o">(</span><span class="s">"WorkerWatcher"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">WorkerWatcher</span><span class="o">(</span><span class="nv">env</span><span class="o">.</span><span class="py">rpcEnv</span><span class="o">,</span> <span class="n">url</span><span class="o">))</span>
<span class="o">}</span>
<span class="nv">env</span><span class="o">.</span><span class="py">rpcEnv</span><span class="o">.</span><span class="py">awaitTermination</span><span class="o">()</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">rpc</code>在<code class="language-plaintext highlighter-rouge">onStart()</code>的时候会发送<code class="language-plaintext highlighter-rouge">RegisterExecutor</code>的请求,用于注册<code class="language-plaintext highlighter-rouge">executor</code>的相关信息。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">onStart</span><span class="o">()</span> <span class="o">{</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="s">"Connecting to driver: "</span> <span class="o">+</span> <span class="n">driverUrl</span><span class="o">)</span>
<span class="nv">rpcEnv</span><span class="o">.</span><span class="py">asyncSetupEndpointRefByURI</span><span class="o">(</span><span class="n">driverUrl</span><span class="o">).</span><span class="py">flatMap</span> <span class="o">{</span> <span class="n">ref</span> <span class="k">=></span>
<span class="c1">// This is a very fast action so we can use "ThreadUtils.sameThread"</span>
<span class="n">driver</span> <span class="k">=</span> <span class="nc">Some</span><span class="o">(</span><span class="n">ref</span><span class="o">)</span>
<span class="nv">ref</span><span class="o">.</span><span class="py">ask</span><span class="o">[</span><span class="kt">Boolean</span><span class="o">](</span><span class="nc">RegisterExecutor</span><span class="o">(</span><span class="n">executorId</span><span class="o">,</span> <span class="n">self</span><span class="o">,</span> <span class="n">hostname</span><span class="o">,</span> <span class="n">cores</span><span class="o">,</span> <span class="n">extractLogUrls</span><span class="o">))</span>
<span class="o">}(</span><span class="nv">ThreadUtils</span><span class="o">.</span><span class="py">sameThread</span><span class="o">).</span><span class="py">onComplete</span> <span class="o">{</span>
<span class="c1">// This is a very fast action so we can use "ThreadUtils.sameThread"</span>
<span class="k">case</span> <span class="nc">Success</span><span class="o">(</span><span class="n">msg</span><span class="o">)</span> <span class="k">=></span>
<span class="c1">// Always receive `true`. Just ignore it</span>
<span class="k">case</span> <span class="nc">Failure</span><span class="o">(</span><span class="n">e</span><span class="o">)</span> <span class="k">=></span>
<span class="nf">exitExecutor</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="n">s</span><span class="s">"Cannot register with driver: $driverUrl"</span><span class="o">,</span> <span class="n">e</span><span class="o">,</span> <span class="n">notifyDriver</span> <span class="k">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="o">}(</span><span class="nv">ThreadUtils</span><span class="o">.</span><span class="py">sameThread</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Driver</code>端<code class="language-plaintext highlighter-rouge">CoarseGrainedSchedulerBackend#receiveAndReply(RpcCallContext)</code>在收到<code class="language-plaintext highlighter-rouge">executor</code>注册请求时,会<code class="language-plaintext highlighter-rouge">reply</code>一个已经注册成功的响应。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">executorRef</span><span class="o">.</span><span class="py">send</span><span class="o">(</span><span class="nc">RegisteredExecutor</span><span class="o">)</span>
<span class="c1">// Note: some tests expect the reply to come after we put the executor in the map</span>
<span class="nv">context</span><span class="o">.</span><span class="py">reply</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
<span class="nv">listenerBus</span><span class="o">.</span><span class="py">post</span><span class="o">(</span>
<span class="nc">SparkListenerExecutorAdded</span><span class="o">(</span><span class="nv">System</span><span class="o">.</span><span class="py">currentTimeMillis</span><span class="o">(),</span> <span class="n">executorId</span><span class="o">,</span> <span class="n">data</span><span class="o">))</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">executor</code>收到响应后会启动一个<code class="language-plaintext highlighter-rouge">exectuor</code>,接下来就是等待<code class="language-plaintext highlighter-rouge">Driver</code>发送过来要进行调度的任务(用<code class="language-plaintext highlighter-rouge">case LaunchTask</code>匹配请求)。<code class="language-plaintext highlighter-rouge">executor</code>执行<code class="language-plaintext highlighter-rouge">launchTask()</code>,创建<code class="language-plaintext highlighter-rouge">TaskRunner</code>任务运行的流程就与<code class="language-plaintext highlighter-rouge">standalone</code>模式相同,<code class="language-plaintext highlighter-rouge">yarn-client</code>模式下<code class="language-plaintext highlighter-rouge">spark</code>任务提交以及运行的流程就是这样。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">receive</span><span class="k">:</span> <span class="kt">PartialFunction</span><span class="o">[</span><span class="kt">Any</span>, <span class="kt">Unit</span><span class="o">]</span> <span class="k">=</span> <span class="o">{</span>
<span class="cm">/* Driver响应executor注册成功时接收的请求 */</span>
<span class="k">case</span> <span class="nc">RegisteredExecutor</span> <span class="k">=></span>
<span class="nf">logInfo</span><span class="o">(</span><span class="s">"Successfully registered with driver"</span><span class="o">)</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">executor</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">Executor</span><span class="o">(</span><span class="n">executorId</span><span class="o">,</span> <span class="n">hostname</span><span class="o">,</span> <span class="n">env</span><span class="o">,</span> <span class="n">userClassPath</span><span class="o">,</span> <span class="n">isLocal</span> <span class="k">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">{</span>
<span class="k">case</span> <span class="nc">NonFatal</span><span class="o">(</span><span class="n">e</span><span class="o">)</span> <span class="k">=></span>
<span class="nf">exitExecutor</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="s">"Unable to create executor due to "</span> <span class="o">+</span> <span class="nv">e</span><span class="o">.</span><span class="py">getMessage</span><span class="o">,</span> <span class="n">e</span><span class="o">)</span>
<span class="o">}</span>
<span class="cm">/* Driver发送过来要进行调度的任务 */</span>
<span class="k">case</span> <span class="nc">LaunchTask</span><span class="o">(</span><span class="n">data</span><span class="o">)</span> <span class="k">=></span>
<span class="nf">if</span> <span class="o">(</span><span class="n">executor</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nf">exitExecutor</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="s">"Received LaunchTask command but executor was null"</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">taskDesc</span> <span class="k">=</span> <span class="nv">TaskDescription</span><span class="o">.</span><span class="py">decode</span><span class="o">(</span><span class="nv">data</span><span class="o">.</span><span class="py">value</span><span class="o">)</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="s">"Got assigned task "</span> <span class="o">+</span> <span class="nv">taskDesc</span><span class="o">.</span><span class="py">taskId</span><span class="o">)</span>
<span class="nv">executor</span><span class="o">.</span><span class="py">launchTask</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">taskDesc</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>接下来分析<code class="language-plaintext highlighter-rouge">spark app</code>在<code class="language-plaintext highlighter-rouge">yarn cluster</code>模式下的启动流程,主要流程和<code class="language-plaintext highlighter-rouge">client</code>模式一样,都是从<code class="language-plaintext highlighter-rouge">SparkSubmit</code>开始分析,启动环境的差异在于<code class="language-plaintext highlighter-rouge">prepareSubmitEnvironment()</code>方法。在<code class="language-plaintext highlighter-rouge">cluster</code>模式下会设置<code class="language-plaintext highlighter-rouge">childMainClass</code>为<code class="language-plaintext highlighter-rouge">org.apache.spark.deploy.yarn.YarnClusterApplication</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// In yarn-cluster mode, use yarn.Client as a wrapper around the user class</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">isYarnCluster</span><span class="o">)</span> <span class="o">{</span>
<span class="n">childMainClass</span> <span class="k">=</span> <span class="nc">YARN_CLUSTER_SUBMIT_CLASS</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">args</span><span class="o">.</span><span class="py">isPython</span><span class="o">)</span> <span class="o">{</span>
<span class="n">childArgs</span> <span class="o">+=</span> <span class="o">(</span><span class="s">"--primary-py-file"</span><span class="o">,</span> <span class="nv">args</span><span class="o">.</span><span class="py">primaryResource</span><span class="o">)</span>
<span class="n">childArgs</span> <span class="o">+=</span> <span class="o">(</span><span class="s">"--class"</span><span class="o">,</span> <span class="s">"org.apache.spark.deploy.PythonRunner"</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="nv">args</span><span class="o">.</span><span class="py">isR</span><span class="o">)</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">mainFile</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">Path</span><span class="o">(</span><span class="nv">args</span><span class="o">.</span><span class="py">primaryResource</span><span class="o">).</span><span class="py">getName</span>
<span class="n">childArgs</span> <span class="o">+=</span> <span class="o">(</span><span class="s">"--primary-r-file"</span><span class="o">,</span> <span class="n">mainFile</span><span class="o">)</span>
<span class="n">childArgs</span> <span class="o">+=</span> <span class="o">(</span><span class="s">"--class"</span><span class="o">,</span> <span class="s">"org.apache.spark.deploy.RRunner"</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">args</span><span class="o">.</span><span class="py">primaryResource</span> <span class="o">!=</span> <span class="nv">SparkLauncher</span><span class="o">.</span><span class="py">NO_RESOURCE</span><span class="o">)</span> <span class="o">{</span>
<span class="n">childArgs</span> <span class="o">+=</span> <span class="o">(</span><span class="s">"--jar"</span><span class="o">,</span> <span class="nv">args</span><span class="o">.</span><span class="py">primaryResource</span><span class="o">)</span>
<span class="o">}</span>
<span class="n">childArgs</span> <span class="o">+=</span> <span class="o">(</span><span class="s">"--class"</span><span class="o">,</span> <span class="nv">args</span><span class="o">.</span><span class="py">mainClass</span><span class="o">)</span>
<span class="o">}</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">args</span><span class="o">.</span><span class="py">childArgs</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">args</span><span class="o">.</span><span class="py">childArgs</span><span class="o">.</span><span class="py">foreach</span> <span class="o">{</span> <span class="n">arg</span> <span class="k">=></span> <span class="n">childArgs</span> <span class="o">+=</span> <span class="o">(</span><span class="s">"--arg"</span><span class="o">,</span> <span class="n">arg</span><span class="o">)</span> <span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">sparkContext</code>创建<code class="language-plaintext highlighter-rouge">taskScheduler</code>时,会设置其<code class="language-plaintext highlighter-rouge">scheduler</code>为<code class="language-plaintext highlighter-rouge">YarnClusterScheduler</code>,<code class="language-plaintext highlighter-rouge">SchedulerBackend</code>为<code class="language-plaintext highlighter-rouge">YarnClusterSchedulerBackend</code>,作为<code class="language-plaintext highlighter-rouge">task</code>调度的容器与<code class="language-plaintext highlighter-rouge">client</code>模式是有差异的。</p>
spark standalone模式启动源码分析
2021-02-18T00:00:00+00:00
https://dongma.github.io/2021/02/18/spark-standalone-mode
<blockquote>
<p>spark目前支持以standalone、Mesos、YARN、Kubernetes等方式部署,本文主要分析apache spark在standalone模式下资源的初始化、用户application的提交,在spark-submit脚本提交应用时,如何将–extraClassPath等参数传递给Driver等相关流程。</p>
</blockquote>
<p>从<code class="language-plaintext highlighter-rouge">spark-submit.sh</code>提交用户<code class="language-plaintext highlighter-rouge">app</code>开始进行分析,<code class="language-plaintext highlighter-rouge">--class</code> 为<code class="language-plaintext highlighter-rouge">jar</code>包中的<code class="language-plaintext highlighter-rouge">main</code>类,<code class="language-plaintext highlighter-rouge">/path/to/examples.jar</code>为用户自定义的<code class="language-plaintext highlighter-rouge">jar</code>包、<code class="language-plaintext highlighter-rouge">1000</code>为运行<code class="language-plaintext highlighter-rouge">SparkPi</code>所需要的参数(基于<code class="language-plaintext highlighter-rouge">spark 2.4.5</code>分析)。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run on a Spark standalone cluster in client deploy mode</span>
./bin/spark-submit <span class="se">\</span>
<span class="nt">--class</span> org.apache.spark.examples.SparkPi <span class="se">\</span>
<span class="nt">--master</span> spark://207.184.161.138:7077 <span class="se">\</span>
<span class="nt">--executor-memory</span> 20G <span class="se">\</span>
<span class="nt">--total-executor-cores</span> 100 <span class="se">\</span>
/path/to/examples.jar <span class="se">\</span>
1000
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">spark</code>的<code class="language-plaintext highlighter-rouge">bin</code>目录下的<code class="language-plaintext highlighter-rouge">spark-submit.sh</code>脚本中存在调用<code class="language-plaintext highlighter-rouge">spark-class.sh</code>,同时会将<code class="language-plaintext highlighter-rouge">spark-submit</code>的参数作为<code class="language-plaintext highlighter-rouge">"$@"</code>进行传递:
<!-- more --></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 在用spark-submit提交程序jar及相应参数时,调用该脚本程序 "$@"为执行脚本的参数,将其传递给spark-class.sh</span>
<span class="nb">exec</span> <span class="s2">"</span><span class="k">${</span><span class="nv">SPARK_HOME</span><span class="k">}</span><span class="s2">"</span>/bin/spark-class org.apache.spark.deploy.SparkSubmit <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">spark-class.sh</code>中会将参数传递给<code class="language-plaintext highlighter-rouge">org.apache.spark.launcher.Main</code>用于启动程序:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># The exit code of the launcher is appended to the output, so the parent shell removes it from the</span>
<span class="c"># command array and checks the value to see if the launcher succeeded.</span>
build_command<span class="o">()</span> <span class="o">{</span>
<span class="s2">"</span><span class="nv">$RUNNER</span><span class="s2">"</span> <span class="nt">-Xmx128m</span> <span class="nt">-cp</span> <span class="s2">"</span><span class="nv">$LAUNCH_CLASSPATH</span><span class="s2">"</span> org.apache.spark.launcher.Main <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
<span class="nb">printf</span> <span class="s2">"%d</span><span class="se">\0</span><span class="s2">"</span> <span class="nv">$?</span>
<span class="o">}</span>
<span class="c"># Turn off posix mode since it does not allow process substitution</span>
<span class="nb">set</span> +o posix
<span class="nv">CMD</span><span class="o">=()</span>
<span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-d</span> <span class="s1">''</span> <span class="nt">-r</span> ARG<span class="p">;</span> <span class="k">do
</span>CMD+<span class="o">=(</span><span class="s2">"</span><span class="nv">$ARG</span><span class="s2">"</span><span class="o">)</span>
<span class="c"># 调用build_command()函数将参数传递给 org.apache.spark.launcher.Main这个类,用于启动用户程序</span>
<span class="k">done</span> < <<span class="o">(</span>build_command <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span><span class="o">)</span>
</code></pre></div></div>
<p>参数传递到<code class="language-plaintext highlighter-rouge">org.apache.spark.launcher.Main#main(String[] argsArray)</code>方法用于触发运行<code class="language-plaintext highlighter-rouge">spark</code>应用程序,当<code class="language-plaintext highlighter-rouge">class</code>为<code class="language-plaintext highlighter-rouge">SparkSubmit</code>时,从<code class="language-plaintext highlighter-rouge">args</code>中解析校验请求参数,校验参数、加载<code class="language-plaintext highlighter-rouge">classpath</code>中的<code class="language-plaintext highlighter-rouge">jar</code>、向<code class="language-plaintext highlighter-rouge">executor</code>申请的资源来构建<code class="language-plaintext highlighter-rouge">bash</code>脚本,触发<code class="language-plaintext highlighter-rouge">spark</code>执行应用程序。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">public</span> <span class="n">static</span> <span class="n">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">argsArray</span><span class="o">)</span> <span class="n">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
<span class="cm">/* 通过spark-submit脚本启动时为此形式,exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@" */</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">className</span><span class="o">.</span><span class="py">equals</span><span class="o">(</span><span class="s">"org.apache.spark.deploy.SparkSubmit"</span><span class="o">))</span> <span class="o">{</span>
<span class="nc">AbstractCommandBuilder</span> <span class="n">builder</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">SparkSubmitCommandBuilder</span><span class="o">(</span><span class="n">args</span><span class="o">);</span>
<span class="cm">/* 从spark-submit.sh中解析请求参数,获取spark参数构建执行命令 AbstractCommandBuilder#buildCommand */</span>
<span class="n">cmd</span> <span class="k">=</span> <span class="nf">buildCommand</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="n">env</span><span class="o">,</span> <span class="n">printLaunchCommand</span><span class="o">);</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="nc">AbstractCommandBuilder</span> <span class="n">builder</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">SparkClassCommandBuilder</span><span class="o">(</span><span class="n">className</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>
<span class="n">cmd</span> <span class="k">=</span> <span class="nf">buildCommand</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="n">env</span><span class="o">,</span> <span class="n">printLaunchCommand</span><span class="o">);</span>
<span class="o">}</span>
<span class="cm">/*
* /usr/latest/bin/java -cp [classpath options] org.apache.spark.deploy.SparkSubmit --master yarn-cluster
* --num-executors 100 --executor-memory 6G --executor-cores 4 --driver-memory 1G --conf spark.default.parallelism=1000
* --conf spark.storage.memoryFraction=0.5 --conf spark.shuffle.memoryFraction=0.3
* */</span>
<span class="nf">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">c</span> <span class="k">:</span> <span class="kt">bashCmd</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">System</span><span class="o">.</span><span class="py">out</span><span class="o">.</span><span class="py">print</span><span class="o">(</span><span class="n">c</span><span class="o">);</span>
<span class="nv">System</span><span class="o">.</span><span class="py">out</span><span class="o">.</span><span class="py">print</span><span class="o">(</span><span class="sc">'\0'</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>进入<code class="language-plaintext highlighter-rouge">org.apache.spark.deploy#main()</code>方法体,<code class="language-plaintext highlighter-rouge">parseArguments(args)</code>方法会解析<code class="language-plaintext highlighter-rouge">spark-submit.class</code>的参数、加载系统环境变量(<code class="language-plaintext highlighter-rouge">ignore spark</code>无关的参数),会调用父类 <code class="language-plaintext highlighter-rouge">SparkSubmitOptionParser#parse(List<String> args)</code>方法解析参数,然后通过<code class="language-plaintext highlighter-rouge">handle()</code>、<code class="language-plaintext highlighter-rouge">handleUnknown()</code>、<code class="language-plaintext highlighter-rouge">handleExtraArgs()</code>获得应用程序需要的<code class="language-plaintext highlighter-rouge">jar</code>(<code class="language-plaintext highlighter-rouge">--jars</code>参数)和参数。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">doSubmit</span><span class="o">(</span><span class="n">args</span><span class="k">:</span> <span class="kt">Array</span><span class="o">[</span><span class="kt">String</span><span class="o">])</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="o">{</span>
<span class="c1">// Initialize logging if it hasn't been done yet. Keep track of whether logging needs to</span>
<span class="c1">// be reset before the application starts.</span>
<span class="k">val</span> <span class="nv">uninitLog</span> <span class="k">=</span> <span class="nf">initializeLogIfNecessary</span><span class="o">(</span><span class="kc">true</span><span class="o">,</span> <span class="n">silent</span> <span class="k">=</span> <span class="kc">true</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">appArgs</span> <span class="k">=</span> <span class="nf">parseArguments</span><span class="o">(</span><span class="n">args</span><span class="o">)</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">appArgs</span><span class="o">.</span><span class="py">verbose</span><span class="o">)</span> <span class="o">{</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="nv">appArgs</span><span class="o">.</span><span class="py">toString</span><span class="o">)</span>
<span class="o">}</span>
<span class="nv">appArgs</span><span class="o">.</span><span class="py">action</span> <span class="k">match</span> <span class="o">{</span>
<span class="k">case</span> <span class="nv">SparkSubmitAction</span><span class="o">.</span><span class="py">SUBMIT</span> <span class="k">=></span> <span class="nf">submit</span><span class="o">(</span><span class="n">appArgs</span><span class="o">,</span> <span class="n">uninitLog</span><span class="o">)</span>
<span class="k">case</span> <span class="nv">SparkSubmitAction</span><span class="o">.</span><span class="py">KILL</span> <span class="k">=></span> <span class="nf">kill</span><span class="o">(</span><span class="n">appArgs</span><span class="o">)</span>
<span class="k">case</span> <span class="nv">SparkSubmitAction</span><span class="o">.</span><span class="py">REQUEST_STATUS</span> <span class="k">=></span> <span class="nf">requestStatus</span><span class="o">(</span><span class="n">appArgs</span><span class="o">)</span>
<span class="k">case</span> <span class="nv">SparkSubmitAction</span><span class="o">.</span><span class="py">PRINT_VERSION</span> <span class="k">=></span> <span class="nf">printVersion</span><span class="o">()</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在提交应用未指定<code class="language-plaintext highlighter-rouge">action</code>参数时,默认为<code class="language-plaintext highlighter-rouge">submit</code>类型,以下为<code class="language-plaintext highlighter-rouge">SparkSubmitArguments#loadEnvironmentArguments()</code>解析的内容</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Action should be SUBMIT unless otherwise specified</span>
<span class="n">action</span> <span class="k">=</span> <span class="nc">Option</span><span class="o">(</span><span class="n">action</span><span class="o">).</span><span class="py">getOrElse</span><span class="o">(</span><span class="nc">SUBMIT</span><span class="o">)</span>
</code></pre></div></div>
<p>继续跟踪到<code class="language-plaintext highlighter-rouge">SparkSubmit#submit(SparkSubmitArguments, Boolean)</code>方法,在<code class="language-plaintext highlighter-rouge">spark 1.3</code>以后逐渐采用<code class="language-plaintext highlighter-rouge">rest</code>协议进行数据通信,直接进入<code class="language-plaintext highlighter-rouge">doRunMain(args: SparkSubmitArguments, uninitLog: Boolean)</code>方法,调用<code class="language-plaintext highlighter-rouge">prepareSubmitEnvironment(args)</code>解析应用程序参数,通过自定义类加载器<code class="language-plaintext highlighter-rouge">MutableURLClassLoader</code>下载<code class="language-plaintext highlighter-rouge">jar</code>包加载<code class="language-plaintext highlighter-rouge">class</code>进入<code class="language-plaintext highlighter-rouge">jvm</code>:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">def</span> <span class="nf">runMain</span><span class="o">(</span><span class="n">args</span><span class="k">:</span> <span class="kt">SparkSubmitArguments</span><span class="o">,</span> <span class="n">uninitLog</span><span class="k">:</span> <span class="kt">Boolean</span><span class="o">)</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="o">{</span>
<span class="nf">val</span> <span class="o">(</span><span class="n">childArgs</span><span class="o">,</span> <span class="n">childClasspath</span><span class="o">,</span> <span class="n">sparkConf</span><span class="o">,</span> <span class="n">childMainClass</span><span class="o">)</span> <span class="k">=</span> <span class="nf">prepareSubmitEnvironment</span><span class="o">(</span><span class="n">args</span><span class="o">)</span>
<span class="cm">/* 设置当前线程的classLoader,MutableURLClassLoader实现了URLClassLoader接口,用于自定义类的加载 */</span>
<span class="k">val</span> <span class="nv">loader</span> <span class="k">=</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">sparkConf</span><span class="o">.</span><span class="py">get</span><span class="o">(</span><span class="nc">DRIVER_USER_CLASS_PATH_FIRST</span><span class="o">))</span> <span class="o">{</span>
<span class="k">new</span> <span class="nc">ChildFirstURLClassLoader</span><span class="o">(</span><span class="k">new</span> <span class="nc">Array</span><span class="o">[</span><span class="kt">URL</span><span class="o">](</span><span class="mi">0</span><span class="o">),</span>
<span class="nv">Thread</span><span class="o">.</span><span class="py">currentThread</span><span class="o">.</span><span class="py">getContextClassLoader</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="k">new</span> <span class="nc">MutableURLClassLoader</span><span class="o">(</span><span class="k">new</span> <span class="nc">Array</span><span class="o">[</span><span class="kt">URL</span><span class="o">](</span><span class="mi">0</span><span class="o">),</span>
<span class="nv">Thread</span><span class="o">.</span><span class="py">currentThread</span><span class="o">.</span><span class="py">getContextClassLoader</span><span class="o">)</span>
<span class="o">}</span>
<span class="cm">/* 线程默认类加载器假如不设置 采用的是系统类加载器,线程上下文加载器会继承其父类加载器 */</span>
<span class="nv">Thread</span><span class="o">.</span><span class="py">currentThread</span><span class="o">.</span><span class="py">setContextClassLoader</span><span class="o">(</span><span class="n">loader</span><span class="o">)</span>
<span class="cm">/* 只有在yarn client模式下,用户的jar、通过--jars上传的jar全部被打包到loader的classpath里面.所以说,只要不少包 无论隐式
* 引用其它包的类还是显式的引用,都会被找到.
* --jars 参数指定的jars在yarn cluster模式下,直接是被封装到childArgs里面了,传递给了yarn.client
*/</span>
<span class="nf">for</span> <span class="o">(</span><span class="n">jar</span> <span class="k"><-</span> <span class="n">childClasspath</span><span class="o">)</span> <span class="o">{</span>
<span class="nf">addJarToClasspath</span><span class="o">(</span><span class="n">jar</span><span class="o">,</span> <span class="n">loader</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">var</span> <span class="n">mainClass</span><span class="k">:</span> <span class="kt">Class</span><span class="o">[</span><span class="k">_</span><span class="o">]</span> <span class="k">=</span> <span class="kc">null</span>
<span class="k">try</span> <span class="o">{</span>
<span class="cm">/* 采用的是上面的类加载器用于加载类class */</span>
<span class="n">mainClass</span> <span class="k">=</span> <span class="nv">Utils</span><span class="o">.</span><span class="py">classForName</span><span class="o">(</span><span class="n">childMainClass</span><span class="o">)</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">{</span>
<span class="k">case</span> <span class="n">e</span><span class="k">:</span> <span class="kt">ClassNotFoundException</span> <span class="o">=></span>
<span class="nf">logWarning</span><span class="o">(</span><span class="n">s</span><span class="s">"Failed to load $childMainClass."</span><span class="o">,</span> <span class="n">e</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">val</span> <span class="nv">app</span><span class="k">:</span> <span class="kt">SparkApplication</span> <span class="o">=</span> <span class="nf">if</span> <span class="o">(</span><span class="n">classOf</span><span class="o">[</span><span class="kt">SparkApplication</span><span class="o">].</span><span class="py">isAssignableFrom</span><span class="o">(</span><span class="n">mainClass</span><span class="o">))</span> <span class="o">{</span>
<span class="nv">mainClass</span><span class="o">.</span><span class="py">newInstance</span><span class="o">().</span><span class="py">asInstanceOf</span><span class="o">[</span><span class="kt">SparkApplication</span><span class="o">]</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="c1">// SPARK-4170</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">classOf</span><span class="o">[</span><span class="kt">scala.App</span><span class="o">].</span><span class="py">isAssignableFrom</span><span class="o">(</span><span class="n">mainClass</span><span class="o">))</span> <span class="o">{</span>
<span class="nf">logWarning</span><span class="o">(</span><span class="s">"Subclasses of scala.App may not work correctly. Use a main() method instead."</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">new</span> <span class="nc">JavaMainApplication</span><span class="o">(</span><span class="n">mainClass</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">try</span> <span class="o">{</span>
<span class="cm">/* standalone模式在 SparkSubmit#prepareSubmitEnvironment(args)中将childMainClass设置为RestSubmissionClient */</span>
<span class="nv">app</span><span class="o">.</span><span class="py">start</span><span class="o">(</span><span class="nv">childArgs</span><span class="o">.</span><span class="py">toArray</span><span class="o">,</span> <span class="n">sparkConf</span><span class="o">)</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">{</span>
<span class="k">case</span> <span class="n">t</span><span class="k">:</span> <span class="kt">Throwable</span> <span class="o">=></span>
<span class="k">throw</span> <span class="nf">findCause</span><span class="o">(</span><span class="n">t</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在之前调用<code class="language-plaintext highlighter-rouge">prepareSubmitEnvironment(args)</code>时已将<code class="language-plaintext highlighter-rouge">mainClass</code>实例化为<code class="language-plaintext highlighter-rouge">RestSubmissionClient</code>,使用<code class="language-plaintext highlighter-rouge">app.start(childArgs.toArray, sparkConf)</code>使用<code class="language-plaintext highlighter-rouge">restclient</code>提交请求,在<code class="language-plaintext highlighter-rouge">RestSubmissionClient.filterSystemEnvironment(sys.env)</code>方法会过滤掉非<code class="language-plaintext highlighter-rouge">SPARK_</code>或<code class="language-plaintext highlighter-rouge">MESOS_</code>开头的环境变量。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">start</span><span class="o">(</span><span class="n">args</span><span class="k">:</span> <span class="kt">Array</span><span class="o">[</span><span class="kt">String</span><span class="o">],</span> <span class="n">conf</span><span class="k">:</span> <span class="kt">SparkConf</span><span class="o">)</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="o">{</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">args</span><span class="o">.</span><span class="py">length</span> <span class="o"><</span> <span class="mi">2</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">sys</span><span class="o">.</span><span class="py">error</span><span class="o">(</span><span class="s">"Usage: RestSubmissionClient [app resource] [main class] [app args*]"</span><span class="o">)</span>
<span class="nv">sys</span><span class="o">.</span><span class="py">exit</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">val</span> <span class="nv">appResource</span> <span class="k">=</span> <span class="nf">args</span><span class="o">(</span><span class="mi">0</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">mainClass</span> <span class="k">=</span> <span class="nf">args</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">appArgs</span> <span class="k">=</span> <span class="nv">args</span><span class="o">.</span><span class="py">slice</span><span class="o">(</span><span class="mi">2</span><span class="o">,</span> <span class="nv">args</span><span class="o">.</span><span class="py">length</span><span class="o">)</span> <span class="cm">/* 参数的顺序是(args.primaryResource(用户jar), args.mainClass, args.childArgs) */</span>
<span class="c1">// 过滤系统中的环境变量,只保留以 SPARK_ or MESOS_开头的环境变量</span>
<span class="k">val</span> <span class="nv">env</span> <span class="k">=</span> <span class="nv">RestSubmissionClient</span><span class="o">.</span><span class="py">filterSystemEnvironment</span><span class="o">(</span><span class="nv">sys</span><span class="o">.</span><span class="py">env</span><span class="o">)</span>
<span class="nf">run</span><span class="o">(</span><span class="n">appResource</span><span class="o">,</span> <span class="n">mainClass</span><span class="o">,</span> <span class="n">appArgs</span><span class="o">,</span> <span class="n">conf</span><span class="o">,</span> <span class="n">env</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p>追踪到<code class="language-plaintext highlighter-rouge">RestSubmissionClientApp#run</code>方法,将<code class="language-plaintext highlighter-rouge">sparkConf</code>转换为<code class="language-plaintext highlighter-rouge">sparkProperties</code>并进行过滤(只保留<code class="language-plaintext highlighter-rouge">spark.</code>开头的属性),继续跟踪<code class="language-plaintext highlighter-rouge">client.createSubmission(submitRequest)</code>提交<code class="language-plaintext highlighter-rouge">rest</code>请求。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/** Submits a request to run the application and return the response. Visible for testing. */</span>
<span class="k">def</span> <span class="nf">run</span><span class="o">(</span>
<span class="n">appResource</span><span class="k">:</span> <span class="kt">String</span><span class="o">,</span>
<span class="n">mainClass</span><span class="k">:</span> <span class="kt">String</span><span class="o">,</span>
<span class="n">appArgs</span><span class="k">:</span> <span class="kt">Array</span><span class="o">[</span><span class="kt">String</span><span class="o">],</span>
<span class="n">conf</span><span class="k">:</span> <span class="kt">SparkConf</span><span class="o">,</span>
<span class="n">env</span><span class="k">:</span> <span class="kt">Map</span><span class="o">[</span><span class="kt">String</span>, <span class="kt">String</span><span class="o">]</span> <span class="k">=</span> <span class="nc">Map</span><span class="o">())</span><span class="k">:</span> <span class="kt">SubmitRestProtocolResponse</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">master</span> <span class="k">=</span> <span class="nv">conf</span><span class="o">.</span><span class="py">getOption</span><span class="o">(</span><span class="s">"spark.master"</span><span class="o">).</span><span class="py">getOrElse</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nc">IllegalArgumentException</span><span class="o">(</span><span class="s">"'spark.master' must be set."</span><span class="o">)</span>
<span class="o">}</span>
<span class="cm">/* SparkConf创建的时候获取的配置 (以spark.开头的), 转换为SparkProperties */</span>
<span class="k">val</span> <span class="nv">sparkProperties</span> <span class="k">=</span> <span class="nv">conf</span><span class="o">.</span><span class="py">getAll</span><span class="o">.</span><span class="py">toMap</span>
<span class="k">val</span> <span class="nv">client</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">RestSubmissionClient</span><span class="o">(</span><span class="n">master</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">submitRequest</span> <span class="k">=</span> <span class="nv">client</span><span class="o">.</span><span class="py">constructSubmitRequest</span><span class="o">(</span>
<span class="n">appResource</span><span class="o">,</span> <span class="n">mainClass</span><span class="o">,</span> <span class="n">appArgs</span><span class="o">,</span> <span class="n">sparkProperties</span><span class="o">,</span> <span class="n">env</span><span class="o">)</span>
<span class="cm">/* 发送创建好的消息Message(submitRequest)到Driver端, postJson(url, request.toJson)解析rest返回的结果 */</span>
<span class="nv">client</span><span class="o">.</span><span class="py">createSubmission</span><span class="o">(</span><span class="n">submitRequest</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">RestSubmissionClientApp#createSubmission()</code>方法中验证所有<code class="language-plaintext highlighter-rouge">masters</code>地址,开始构建<code class="language-plaintext highlighter-rouge">submitUrl</code>然后逐个向<code class="language-plaintext highlighter-rouge">master</code>发送请求。在每次发送请求时都会验证<code class="language-plaintext highlighter-rouge">master</code>是否可用,当不可用时会将其添加到<code class="language-plaintext highlighter-rouge">lostMasters</code>列表中。至此,在<code class="language-plaintext highlighter-rouge">standalone</code>模式下提交一个<code class="language-plaintext highlighter-rouge">spark application</code>的流程就到此为止。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Submit an application specified by the parameters in the provided request.
* If the submission was successful, poll the status of the submission and report
* it to the user. Otherwise, report the error message provided by the server.
*/</span>
<span class="k">def</span> <span class="nf">createSubmission</span><span class="o">(</span><span class="n">request</span><span class="k">:</span> <span class="kt">CreateSubmissionRequest</span><span class="o">)</span><span class="k">:</span> <span class="kt">SubmitRestProtocolResponse</span> <span class="o">=</span> <span class="o">{</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="n">s</span><span class="s">"Submitting a request to launch an application in $master."</span><span class="o">)</span>
<span class="k">var</span> <span class="n">handled</span><span class="k">:</span> <span class="kt">Boolean</span> <span class="o">=</span> <span class="kc">false</span>
<span class="k">var</span> <span class="n">response</span><span class="k">:</span> <span class="kt">SubmitRestProtocolResponse</span> <span class="o">=</span> <span class="kc">null</span>
<span class="nf">for</span> <span class="o">(</span><span class="n">m</span> <span class="k"><-</span> <span class="n">masters</span> <span class="k">if</span> <span class="o">!</span><span class="n">handled</span><span class="o">)</span> <span class="o">{</span>
<span class="nf">validateMaster</span><span class="o">(</span><span class="n">m</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">url</span> <span class="k">=</span> <span class="nf">getSubmitUrl</span><span class="o">(</span><span class="n">m</span><span class="o">)</span>
<span class="n">response</span> <span class="k">=</span> <span class="nf">postJson</span><span class="o">(</span><span class="n">url</span><span class="o">,</span> <span class="nv">request</span><span class="o">.</span><span class="py">toJson</span><span class="o">)</span>
<span class="n">response</span> <span class="k">match</span> <span class="o">{</span>
<span class="k">case</span> <span class="n">s</span><span class="k">:</span> <span class="kt">CreateSubmissionResponse</span> <span class="o">=></span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">s</span><span class="o">.</span><span class="py">success</span><span class="o">)</span> <span class="o">{</span>
<span class="nf">reportSubmissionStatus</span><span class="o">(</span><span class="n">s</span><span class="o">)</span>
<span class="nf">handleRestResponse</span><span class="o">(</span><span class="n">s</span><span class="o">)</span>
<span class="n">handled</span> <span class="k">=</span> <span class="kc">true</span>
<span class="o">}</span>
<span class="k">case</span> <span class="n">unexpected</span> <span class="k">=></span>
<span class="nf">handleUnexpectedRestResponse</span><span class="o">(</span><span class="n">unexpected</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">response</span>
<span class="o">}</span>
</code></pre></div></div>
<p>客户端提交应用的部分看完了,现在来分析<code class="language-plaintext highlighter-rouge">master</code>端如何接收请求并进行处理,在<code class="language-plaintext highlighter-rouge">start-master.sh</code>脚本中存在以下脚本,可以以<code class="language-plaintext highlighter-rouge">org.apache.spark.deploy.master.Master</code>作为分析代码的入口。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># NOTE: This exact class name is matched downstream by SparkSubmit.</span>
<span class="c"># Any changes need to be reflected there.</span>
<span class="nv">CLASS</span><span class="o">=</span><span class="s2">"org.apache.spark.deploy.master.Master"</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$SPARK_MASTER_WEBUI_PORT</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">""</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">SPARK_MASTER_WEBUI_PORT</span><span class="o">=</span>8080
<span class="k">fi</span>
<span class="s2">"</span><span class="k">${</span><span class="nv">SPARK_HOME</span><span class="k">}</span><span class="s2">/sbin"</span>/spark-daemon.sh start <span class="nv">$CLASS</span> 1 <span class="se">\</span>
<span class="nt">--host</span> <span class="nv">$SPARK_MASTER_HOST</span> <span class="nt">--port</span> <span class="nv">$SPARK_MASTER_PORT</span> <span class="nt">--webui-port</span> <span class="nv">$SPARK_MASTER_WEBUI_PORT</span> <span class="se">\</span>
<span class="nv">$ORIGINAL_ARGS</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">Master#main</code>方法中启动了<code class="language-plaintext highlighter-rouge">RPC</code>运行环境以及<code class="language-plaintext highlighter-rouge">Endpoint</code>,<code class="language-plaintext highlighter-rouge">RpcEndpoint</code>:<code class="language-plaintext highlighter-rouge">RPC</code>端点 ,<code class="language-plaintext highlighter-rouge">Spark</code>针对于每个节点(<code class="language-plaintext highlighter-rouge">Client/Master/Worker</code>)都称之一个<code class="language-plaintext highlighter-rouge">Rpc</code>端点,且都实现<code class="language-plaintext highlighter-rouge">RpcEndpoint</code>接口,内部根据不同端点的需求,设计不同的消息和不同的业务处理,如果需要发送(询问)则调用<code class="language-plaintext highlighter-rouge">Dispatcher</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">main</span><span class="o">(</span><span class="n">argStrings</span><span class="k">:</span> <span class="kt">Array</span><span class="o">[</span><span class="kt">String</span><span class="o">])</span> <span class="o">{</span>
<span class="nv">Thread</span><span class="o">.</span><span class="py">setDefaultUncaughtExceptionHandler</span><span class="o">(</span><span class="k">new</span> <span class="nc">SparkUncaughtExceptionHandler</span><span class="o">(</span>
<span class="n">exitOnUncaughtException</span> <span class="k">=</span> <span class="kc">false</span><span class="o">))</span>
<span class="nv">Utils</span><span class="o">.</span><span class="py">initDaemon</span><span class="o">(</span><span class="n">log</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">conf</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">SparkConf</span>
<span class="k">val</span> <span class="nv">args</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">MasterArguments</span><span class="o">(</span><span class="n">argStrings</span><span class="o">,</span> <span class="n">conf</span><span class="o">)</span>
<span class="cm">/* 创建rpc环境 和 Endpoint(供Rpc调用),在Spark中 Driver, Master ,Worker角色都有各自的Endpoint,相当于各自的Inbox */</span>
<span class="nf">val</span> <span class="o">(</span><span class="n">rpcEnv</span><span class="o">,</span> <span class="k">_</span><span class="o">,</span> <span class="k">_</span><span class="o">)</span> <span class="k">=</span> <span class="nf">startRpcEnvAndEndpoint</span><span class="o">(</span><span class="nv">args</span><span class="o">.</span><span class="py">host</span><span class="o">,</span> <span class="nv">args</span><span class="o">.</span><span class="py">port</span><span class="o">,</span> <span class="nv">args</span><span class="o">.</span><span class="py">webUiPort</span><span class="o">,</span> <span class="n">conf</span><span class="o">)</span>
<span class="nv">rpcEnv</span><span class="o">.</span><span class="py">awaitTermination</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Master</code>继承了<code class="language-plaintext highlighter-rouge">ThreadSafeRpcEndpoint</code>类,重写的<code class="language-plaintext highlighter-rouge">receive</code>方法用于接收<code class="language-plaintext highlighter-rouge">netty</code>提交的请求,这部分为<code class="language-plaintext highlighter-rouge">Master</code>服务启动的过程。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">receive</span><span class="k">:</span> <span class="kt">PartialFunction</span><span class="o">[</span><span class="kt">Any</span>, <span class="kt">Unit</span><span class="o">]</span> <span class="k">=</span> <span class="o">{</span>
<span class="cm">/* 在AppClient向master注册Application后才会触发master的schedule函数进行launchExecutors操作 */</span>
<span class="k">case</span> <span class="nc">RegisterApplication</span><span class="o">(</span><span class="n">description</span><span class="o">,</span> <span class="n">driver</span><span class="o">)</span> <span class="k">=></span>
<span class="c1">// TODO Prevent repeated registrations from some driver</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">state</span> <span class="o">==</span> <span class="nv">RecoveryState</span><span class="o">.</span><span class="py">STANDBY</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// ignore, don't send response</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="s">"Registering app "</span> <span class="o">+</span> <span class="nv">description</span><span class="o">.</span><span class="py">name</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">app</span> <span class="k">=</span> <span class="nf">createApplication</span><span class="o">(</span><span class="n">description</span><span class="o">,</span> <span class="n">driver</span><span class="o">)</span>
<span class="nf">registerApplication</span><span class="o">(</span><span class="n">app</span><span class="o">)</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="s">"Registered app "</span> <span class="o">+</span> <span class="nv">description</span><span class="o">.</span><span class="py">name</span> <span class="o">+</span> <span class="s">" with ID "</span> <span class="o">+</span> <span class="nv">app</span><span class="o">.</span><span class="py">id</span><span class="o">)</span>
<span class="nv">persistenceEngine</span><span class="o">.</span><span class="py">addApplication</span><span class="o">(</span><span class="n">app</span><span class="o">)</span>
<span class="nv">driver</span><span class="o">.</span><span class="py">send</span><span class="o">(</span><span class="nc">RegisteredApplication</span><span class="o">(</span><span class="nv">app</span><span class="o">.</span><span class="py">id</span><span class="o">,</span> <span class="n">self</span><span class="o">))</span>
<span class="nf">schedule</span><span class="o">()</span> <span class="cm">/* todo: 用于调度Driver,具体的调度内容需要详细的看 */</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">RestSubmissionClient</code>提交的请求统一由<code class="language-plaintext highlighter-rouge">StandaloneRestServer#handleSubmit(String, SubmitRestProtocolMessage, HttpServletResponse)</code>统一进行处理,通过<code class="language-plaintext highlighter-rouge">case CreateSubmissionRequest</code>表达式匹配请求的类型,使用<code class="language-plaintext highlighter-rouge">DeployMessages.RequestSubmitDriver(driverDescription)</code>申请启动<code class="language-plaintext highlighter-rouge">Driver</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// A server that responds to requests submitted by the [[RestSubmissionClient]].</span>
<span class="c1">// This is intended to be embedded in the standalone Master and used in cluster mode only.</span>
<span class="k">protected</span> <span class="k">override</span> <span class="k">def</span> <span class="nf">handleSubmit</span><span class="o">(</span>
<span class="n">requestMessageJson</span><span class="k">:</span> <span class="kt">String</span><span class="o">,</span>
<span class="n">requestMessage</span><span class="k">:</span> <span class="kt">SubmitRestProtocolMessage</span><span class="o">,</span>
<span class="n">responseServlet</span><span class="k">:</span> <span class="kt">HttpServletResponse</span><span class="o">)</span><span class="k">:</span> <span class="kt">SubmitRestProtocolResponse</span> <span class="o">=</span> <span class="o">{</span>
<span class="n">requestMessage</span> <span class="k">match</span> <span class="o">{</span>
<span class="k">case</span> <span class="n">submitRequest</span><span class="k">:</span> <span class="kt">CreateSubmissionRequest</span> <span class="o">=></span>
<span class="cm">/* 构建好所有的参数DriverDescription,用于向Driver端发送请求 */</span>
<span class="k">val</span> <span class="nv">driverDescription</span> <span class="k">=</span> <span class="nf">buildDriverDescription</span><span class="o">(</span><span class="n">submitRequest</span><span class="o">)</span>
<span class="cm">/* Driver构建完成后正式向Master发起一个请求,向master请求资源 */</span>
<span class="k">val</span> <span class="nv">response</span> <span class="k">=</span> <span class="nv">masterEndpoint</span><span class="o">.</span><span class="py">askSync</span><span class="o">[</span><span class="kt">DeployMessages.SubmitDriverResponse</span><span class="o">](</span>
<span class="nv">DeployMessages</span><span class="o">.</span><span class="py">RequestSubmitDriver</span><span class="o">(</span><span class="n">driverDescription</span><span class="o">))</span>
<span class="k">val</span> <span class="nv">submitResponse</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">CreateSubmissionResponse</span>
<span class="nv">submitResponse</span><span class="o">.</span><span class="py">serverSparkVersion</span> <span class="k">=</span> <span class="n">sparkVersion</span>
<span class="nv">submitResponse</span><span class="o">.</span><span class="py">message</span> <span class="k">=</span> <span class="nv">response</span><span class="o">.</span><span class="py">message</span>
<span class="nv">submitResponse</span><span class="o">.</span><span class="py">success</span> <span class="k">=</span> <span class="nv">response</span><span class="o">.</span><span class="py">success</span>
<span class="nv">submitResponse</span><span class="o">.</span><span class="py">submissionId</span> <span class="k">=</span> <span class="nv">response</span><span class="o">.</span><span class="py">driverId</span><span class="o">.</span><span class="py">orNull</span>
<span class="k">val</span> <span class="nv">unknownFields</span> <span class="k">=</span> <span class="nf">findUnknownFields</span><span class="o">(</span><span class="n">requestMessageJson</span><span class="o">,</span> <span class="n">requestMessage</span><span class="o">)</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">unknownFields</span><span class="o">.</span><span class="py">nonEmpty</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// If there are fields that the server does not know about, warn the client</span>
<span class="nv">submitResponse</span><span class="o">.</span><span class="py">unknownFields</span> <span class="k">=</span> <span class="n">unknownFields</span>
<span class="o">}</span>
<span class="n">submitResponse</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">Master#receiveAndReply()</code>方法中用<code class="language-plaintext highlighter-rouge">createDriver(description)</code>对<code class="language-plaintext highlighter-rouge">DriverDescription</code>再进行一次封装,同时通过<code class="language-plaintext highlighter-rouge">schedule()</code>进行资源调度到<code class="language-plaintext highlighter-rouge">Worker</code>上(在<code class="language-plaintext highlighter-rouge">schedule</code>方法中调用<code class="language-plaintext highlighter-rouge">launchDriver</code>的方法,会向<code class="language-plaintext highlighter-rouge">Worker</code>发送一个<code class="language-plaintext highlighter-rouge">LaunchDriver</code>类型请求),最后<code class="language-plaintext highlighter-rouge">reply</code>进行<code class="language-plaintext highlighter-rouge">rest</code>请求响应。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">receiveAndReply</span><span class="o">(</span><span class="n">context</span><span class="k">:</span> <span class="kt">RpcCallContext</span><span class="o">)</span><span class="k">:</span> <span class="kt">PartialFunction</span><span class="o">[</span><span class="kt">Any</span>, <span class="kt">Unit</span><span class="o">]</span> <span class="k">=</span> <span class="o">{</span>
<span class="k">case</span> <span class="nc">RequestSubmitDriver</span><span class="o">(</span><span class="n">description</span><span class="o">)</span> <span class="k">=></span>
<span class="nf">if</span> <span class="o">(</span><span class="n">state</span> <span class="o">!=</span> <span class="nv">RecoveryState</span><span class="o">.</span><span class="py">ALIVE</span><span class="o">)</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">msg</span> <span class="k">=</span> <span class="n">s</span><span class="s">"${Utils.BACKUP_STANDALONE_MASTER_PREFIX}: $state. "</span> <span class="o">+</span>
<span class="s">"Can only accept driver submissions in ALIVE state."</span>
<span class="nv">context</span><span class="o">.</span><span class="py">reply</span><span class="o">(</span><span class="nc">SubmitDriverResponse</span><span class="o">(</span><span class="n">self</span><span class="o">,</span> <span class="kc">false</span><span class="o">,</span> <span class="nc">None</span><span class="o">,</span> <span class="n">msg</span><span class="o">))</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="s">"Driver submitted "</span> <span class="o">+</span> <span class="nv">description</span><span class="o">.</span><span class="py">command</span><span class="o">.</span><span class="py">mainClass</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">driver</span> <span class="k">=</span> <span class="nf">createDriver</span><span class="o">(</span><span class="n">description</span><span class="o">)</span>
<span class="nv">persistenceEngine</span><span class="o">.</span><span class="py">addDriver</span><span class="o">(</span><span class="n">driver</span><span class="o">)</span>
<span class="n">waitingDrivers</span> <span class="o">+=</span> <span class="n">driver</span>
<span class="nv">drivers</span><span class="o">.</span><span class="py">add</span><span class="o">(</span><span class="n">driver</span><span class="o">)</span>
<span class="nf">schedule</span><span class="o">()</span> <span class="c1">// 执行调度的逻辑schedule()</span>
<span class="c1">// TODO: It might be good to instead have the submission client poll the master to determine</span>
<span class="c1">// the current status of the driver. For now it's simply "fire and forget".</span>
<span class="nv">context</span><span class="o">.</span><span class="py">reply</span><span class="o">(</span><span class="nc">SubmitDriverResponse</span><span class="o">(</span><span class="n">self</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="nc">Some</span><span class="o">(</span><span class="nv">driver</span><span class="o">.</span><span class="py">id</span><span class="o">),</span>
<span class="n">s</span><span class="s">"Driver successfully submitted as ${driver.id}"</span><span class="o">))</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>将视角转到<code class="language-plaintext highlighter-rouge">Worker#receive()</code>方法中,通过模式匹配<code class="language-plaintext highlighter-rouge">case LaunchDriver(driverId, driverDesc)</code>进入如下代码,然后调用<code class="language-plaintext highlighter-rouge">driver.start()</code>启动程序。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="nc">LaunchDriver</span><span class="o">(</span><span class="n">driverId</span><span class="o">,</span> <span class="n">driverDesc</span><span class="o">)</span> <span class="k">=></span>
<span class="nf">logInfo</span><span class="o">(</span><span class="n">s</span><span class="s">"Asked to launch driver $driverId"</span><span class="o">)</span>
<span class="cm">/*
* 在RestSubmissionClient向StandaloneRestServer提交launchDriver请求后,实际上在StandaloneRestServer进行了一层封装
* DriverWrapper. 所以,在此处启动的类是DriverWrapper 而不是用户程序本身,在该main方法里,主要是用自定义类加载器加载了用户的
* main方法,然后开始启动用户程序 初始化sparkContext等;
*/</span>
<span class="k">val</span> <span class="nv">driver</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">DriverRunner</span><span class="o">(</span>
<span class="n">conf</span><span class="o">,</span>
<span class="n">driverId</span><span class="o">,</span>
<span class="n">workDir</span><span class="o">,</span>
<span class="n">sparkHome</span><span class="o">,</span>
<span class="cm">/*
* 此处的Command就是在StandaloneRestServer封装好的
* val command = new Command("org.apache.spark.deploy.worker.DriverWrapper", Seq("",
* "", mainClass)) ++ appArgs, // args to the DriverWrapper
*/</span>
<span class="nv">driverDesc</span><span class="o">.</span><span class="py">copy</span><span class="o">(</span><span class="n">command</span> <span class="k">=</span> <span class="nv">Worker</span><span class="o">.</span><span class="py">maybeUpdateSSLSettings</span><span class="o">(</span><span class="nv">driverDesc</span><span class="o">.</span><span class="py">command</span><span class="o">,</span> <span class="n">conf</span><span class="o">)),</span>
<span class="n">self</span><span class="o">,</span>
<span class="n">workerUri</span><span class="o">,</span>
<span class="n">securityMgr</span><span class="o">)</span>
<span class="nf">drivers</span><span class="o">(</span><span class="n">driverId</span><span class="o">)</span> <span class="k">=</span> <span class="n">driver</span>
<span class="nv">driver</span><span class="o">.</span><span class="py">start</span><span class="o">()</span>
<span class="n">coresUsed</span> <span class="o">+=</span> <span class="nv">driverDesc</span><span class="o">.</span><span class="py">cores</span>
<span class="n">memoryUsed</span> <span class="o">+=</span> <span class="nv">driverDesc</span><span class="o">.</span><span class="py">mem</span>
</code></pre></div></div>
<p>进入<code class="language-plaintext highlighter-rouge">driver.start()</code>方法,应用会创建<code class="language-plaintext highlighter-rouge">Driver</code>所需要的工作目录,同时<code class="language-plaintext highlighter-rouge">download</code>用户自定义的<code class="language-plaintext highlighter-rouge">jar</code>包 然后开始运行<code class="language-plaintext highlighter-rouge">Driver</code>。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/** Starts a thread to run and manage the driver. */</span>
<span class="k">private</span><span class="o">[</span><span class="kt">worker</span><span class="o">]</span> <span class="k">def</span> <span class="nf">start</span><span class="o">()</span> <span class="k">=</span> <span class="o">{</span>
<span class="k">new</span> <span class="nc">Thread</span><span class="o">(</span><span class="s">"DriverRunner for "</span> <span class="o">+</span> <span class="n">driverId</span><span class="o">)</span> <span class="o">{</span>
<span class="k">override</span> <span class="k">def</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// prepare driver jars and run driver, 下载用户自定义的jar包, buildProcessBuilder该方法有两个默认值的备用参数,主要是准备程序运行的环境 (但并不包含app所在的jar)</span>
<span class="k">val</span> <span class="nv">exitCode</span> <span class="k">=</span> <span class="nf">prepareAndRunDriver</span><span class="o">()</span>
<span class="c1">// set final state depending on if forcibly killed and process exit code</span>
<span class="n">finalState</span> <span class="k">=</span> <span class="nf">if</span> <span class="o">(</span><span class="n">exitCode</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Some</span><span class="o">(</span><span class="nv">DriverState</span><span class="o">.</span><span class="py">FINISHED</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">killed</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Some</span><span class="o">(</span><span class="nv">DriverState</span><span class="o">.</span><span class="py">KILLED</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="nc">Some</span><span class="o">(</span><span class="nv">DriverState</span><span class="o">.</span><span class="py">FAILED</span><span class="o">)</span>
<span class="o">}</span>
<span class="c1">// notify worker of final driver state, possible exception</span>
<span class="nv">worker</span><span class="o">.</span><span class="py">send</span><span class="o">(</span><span class="nc">DriverStateChanged</span><span class="o">(</span><span class="n">driverId</span><span class="o">,</span> <span class="nv">finalState</span><span class="o">.</span><span class="py">get</span><span class="o">,</span> <span class="n">finalException</span><span class="o">))</span>
<span class="o">}</span>
<span class="o">}.</span><span class="py">start</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>进一步进入到<code class="language-plaintext highlighter-rouge">prepareAndRunDriver()</code>方法,程序使用<code class="language-plaintext highlighter-rouge">CommandUtils.buildProcessBuilder()</code>结合<code class="language-plaintext highlighter-rouge">command</code>所要运行的环境,重新构建一个命令。例如: 本地环境变量、系统<code class="language-plaintext highlighter-rouge">classpath</code>, 替换掉传递过来的占位符。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span><span class="o">[</span><span class="kt">worker</span><span class="o">]</span> <span class="k">def</span> <span class="nf">prepareAndRunDriver</span><span class="o">()</span><span class="k">:</span> <span class="kt">Int</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">driverDir</span> <span class="k">=</span> <span class="nf">createWorkingDirectory</span><span class="o">()</span>
<span class="k">val</span> <span class="nv">localJarFilename</span> <span class="k">=</span> <span class="nf">downloadUserJar</span><span class="o">(</span><span class="n">driverDir</span><span class="o">)</span> <span class="c1">// 下载用户自定义的jar包</span>
<span class="k">def</span> <span class="nf">substituteVariables</span><span class="o">(</span><span class="n">argument</span><span class="k">:</span> <span class="kt">String</span><span class="o">)</span><span class="k">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="n">argument</span> <span class="k">match</span> <span class="o">{</span>
<span class="k">case</span> <span class="s">""</span> <span class="k">=></span> <span class="n">workerUrl</span>
<span class="k">case</span> <span class="s">""</span> <span class="k">=></span> <span class="n">localJarFilename</span>
<span class="k">case</span> <span class="n">other</span> <span class="k">=></span> <span class="n">other</span>
<span class="o">}</span>
<span class="c1">// TODO: If we add ability to submit multiple jars they should also be added here</span>
<span class="cm">/* buildProcessBuilder该方法有两个默认值的备用参数,主要是准备程序运行的环境 (但并不包含app所在的jar) */</span>
<span class="k">val</span> <span class="nv">builder</span> <span class="k">=</span> <span class="nv">CommandUtils</span><span class="o">.</span><span class="py">buildProcessBuilder</span><span class="o">(</span><span class="nv">driverDesc</span><span class="o">.</span><span class="py">command</span><span class="o">,</span> <span class="n">securityManager</span><span class="o">,</span>
<span class="nv">driverDesc</span><span class="o">.</span><span class="py">mem</span><span class="o">,</span> <span class="nv">sparkHome</span><span class="o">.</span><span class="py">getAbsolutePath</span><span class="o">,</span> <span class="n">substituteVariables</span><span class="o">)</span>
<span class="nf">runDriver</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="n">driverDir</span><span class="o">,</span> <span class="nv">driverDesc</span><span class="o">.</span><span class="py">supervise</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p>进入<code class="language-plaintext highlighter-rouge">CommandUtils#buildLocalCommand</code>方法,<code class="language-plaintext highlighter-rouge">-cp</code>参数是在<code class="language-plaintext highlighter-rouge">buildCommandSeq(Command, Int, String)</code>中构建。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="cm">/**
* Build a command based on the given one, taking into account the local environment
* of where this command is expected to run, substitute any placeholders, and append
* any extra class paths.
*/</span>
<span class="k">private</span> <span class="k">def</span> <span class="nf">buildLocalCommand</span><span class="o">(</span>
<span class="n">command</span><span class="k">:</span> <span class="kt">Command</span><span class="o">,</span>
<span class="n">securityMgr</span><span class="k">:</span> <span class="kt">SecurityManager</span><span class="o">,</span>
<span class="n">substituteArguments</span><span class="k">:</span> <span class="kt">String</span> <span class="o">=></span> <span class="nc">String</span><span class="o">,</span>
<span class="n">classPath</span><span class="k">:</span> <span class="kt">Seq</span><span class="o">[</span><span class="kt">String</span><span class="o">]</span> <span class="k">=</span> <span class="nv">Seq</span><span class="o">.</span><span class="py">empty</span><span class="o">,</span>
<span class="n">env</span><span class="k">:</span> <span class="kt">Map</span><span class="o">[</span><span class="kt">String</span>, <span class="kt">String</span><span class="o">])</span><span class="k">:</span> <span class="kt">Command</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">libraryPathName</span> <span class="k">=</span> <span class="nv">Utils</span><span class="o">.</span><span class="py">libraryPathEnvName</span> <span class="c1">// 返回系统的path,也就是一些</span>
<span class="k">val</span> <span class="nv">libraryPathEntries</span> <span class="k">=</span> <span class="nv">command</span><span class="o">.</span><span class="py">libraryPathEntries</span>
<span class="k">val</span> <span class="nv">cmdLibraryPath</span> <span class="k">=</span> <span class="nv">command</span><span class="o">.</span><span class="py">environment</span><span class="o">.</span><span class="py">get</span><span class="o">(</span><span class="n">libraryPathName</span><span class="o">)</span>
<span class="k">var</span> <span class="n">newEnvironment</span> <span class="k">=</span> <span class="nf">if</span> <span class="o">(</span><span class="nv">libraryPathEntries</span><span class="o">.</span><span class="py">nonEmpty</span> <span class="o">&&</span> <span class="nv">libraryPathName</span><span class="o">.</span><span class="py">nonEmpty</span><span class="o">)</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">libraryPaths</span> <span class="k">=</span> <span class="n">libraryPathEntries</span> <span class="o">++</span> <span class="n">cmdLibraryPath</span> <span class="o">++</span> <span class="nv">env</span><span class="o">.</span><span class="py">get</span><span class="o">(</span><span class="n">libraryPathName</span><span class="o">)</span>
<span class="nv">command</span><span class="o">.</span><span class="py">environment</span> <span class="o">+</span> <span class="o">((</span><span class="n">libraryPathName</span><span class="o">,</span> <span class="nv">libraryPaths</span><span class="o">.</span><span class="py">mkString</span><span class="o">(</span><span class="nv">File</span><span class="o">.</span><span class="py">pathSeparator</span><span class="o">)))</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="cm">/*
* RestSubmissionClient发送过来的环境变量只有 SPARK_和MESOS_ 开头的环境变量,也即是对于driver端System.getenv()系统环境变量获取
* 的值. 如spark-env初始化的 SPARK_ 开头的环境变量,在提交的时候已经创建好了;
*/</span>
<span class="nv">command</span><span class="o">.</span><span class="py">environment</span>
<span class="o">}</span>
<span class="nc">Command</span><span class="o">(</span>
<span class="cm">/*
* 对于driver并不是用户命令的入口,而是一个封装类org.apache.spark.deploy.DriverWrapper, 在封装类里面进一步解析
* 对于executor是这个org.apache.spark.executor.CoarseGrainedExecutorBackend类
*/</span>
<span class="nv">command</span><span class="o">.</span><span class="py">mainClass</span><span class="o">,</span>
<span class="nv">command</span><span class="o">.</span><span class="py">arguments</span><span class="o">.</span><span class="py">map</span><span class="o">(</span><span class="n">substituteArguments</span><span class="o">),</span>
<span class="n">newEnvironment</span><span class="o">,</span>
<span class="nv">command</span><span class="o">.</span><span class="py">classPathEntries</span> <span class="o">++</span> <span class="n">classPath</span><span class="o">,</span>
<span class="nv">Seq</span><span class="o">.</span><span class="py">empty</span><span class="o">,</span> <span class="c1">// library path already captured in environment variable</span>
<span class="c1">// filter out auth secret from java options</span>
<span class="nv">command</span><span class="o">.</span><span class="py">javaOpts</span><span class="o">.</span><span class="py">filterNot</span><span class="o">(</span><span class="nv">_</span><span class="o">.</span><span class="py">startsWith</span><span class="o">(</span><span class="s">"-D"</span> <span class="o">+</span> <span class="nv">SecurityManager</span><span class="o">.</span><span class="py">SPARK_AUTH_SECRET_CONF</span><span class="o">)))</span> <span class="c1">// spark.jars在此处</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">StandaloneRestServer#buildDriverDescription()</code>方法里指明如何构建<code class="language-plaintext highlighter-rouge">Command</code>类型,用命令行执行的是<code class="language-plaintext highlighter-rouge">org.apache.spark.deploy.worker.DriverWrapper</code>包装类。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* 直接执行的是这个封装类,通过自定义urlClassLoader指定classpath的方式加载用户的jar然后通过反射执行 */</span>
<span class="k">val</span> <span class="nv">command</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">Command</span><span class="o">(</span>
<span class="s">"org.apache.spark.deploy.worker.DriverWrapper"</span><span class="o">,</span>
<span class="nc">Seq</span><span class="o">(</span><span class="s">""</span><span class="o">,</span> <span class="s">""</span><span class="o">,</span> <span class="n">mainClass</span><span class="o">)</span> <span class="o">++</span> <span class="n">appArgs</span><span class="o">,</span> <span class="c1">// args to the DriverWrapper</span>
<span class="n">environmentVariables</span><span class="o">,</span> <span class="n">extraClassPath</span><span class="o">,</span> <span class="n">extraLibraryPath</span><span class="o">,</span> <span class="n">javaOpts</span><span class="o">)</span> <span class="c1">// 也即是此时spark.jars也即--jars传来的参数在javaOpts里面</span>
</code></pre></div></div>
<p>进入到<code class="language-plaintext highlighter-rouge">DriverManager#main(args: Array[String])</code>方法,通过自定义的<code class="language-plaintext highlighter-rouge">classLoader</code>加载<code class="language-plaintext highlighter-rouge">jar</code>包,根据<code class="language-plaintext highlighter-rouge">mainClass</code>通过反射执行其<code class="language-plaintext highlighter-rouge">main()</code>方法,触发用户程序的执行。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">main</span><span class="o">(</span><span class="n">args</span><span class="k">:</span> <span class="kt">Array</span><span class="o">[</span><span class="kt">String</span><span class="o">])</span> <span class="o">{</span>
<span class="k">case</span> <span class="n">workerUrl</span> <span class="o">::</span> <span class="n">userJar</span> <span class="o">::</span> <span class="n">mainClass</span> <span class="o">::</span> <span class="n">extraArgs</span> <span class="k">=></span>
<span class="k">val</span> <span class="nv">conf</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">SparkConf</span><span class="o">()</span>
<span class="k">val</span> <span class="nv">host</span><span class="k">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="nv">Utils</span><span class="o">.</span><span class="py">localHostName</span><span class="o">()</span>
<span class="k">val</span> <span class="nv">port</span><span class="k">:</span> <span class="kt">Int</span> <span class="o">=</span> <span class="nv">sys</span><span class="o">.</span><span class="py">props</span><span class="o">.</span><span class="py">getOrElse</span><span class="o">(</span><span class="s">"spark.driver.port"</span><span class="o">,</span> <span class="s">"0"</span><span class="o">).</span><span class="py">toInt</span>
<span class="k">val</span> <span class="nv">rpcEnv</span> <span class="k">=</span> <span class="nv">RpcEnv</span><span class="o">.</span><span class="py">create</span><span class="o">(</span><span class="s">"Driver"</span><span class="o">,</span> <span class="n">host</span><span class="o">,</span> <span class="n">port</span><span class="o">,</span> <span class="n">conf</span><span class="o">,</span> <span class="k">new</span> <span class="nc">SecurityManager</span><span class="o">(</span><span class="n">conf</span><span class="o">))</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="n">s</span><span class="s">"Driver address: ${rpcEnv.address}"</span><span class="o">)</span>
<span class="nv">rpcEnv</span><span class="o">.</span><span class="py">setupEndpoint</span><span class="o">(</span><span class="s">"workerWatcher"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">WorkerWatcher</span><span class="o">(</span><span class="n">rpcEnv</span><span class="o">,</span> <span class="n">workerUrl</span><span class="o">))</span>
<span class="k">val</span> <span class="nv">currentLoader</span> <span class="k">=</span> <span class="nv">Thread</span><span class="o">.</span><span class="py">currentThread</span><span class="o">.</span><span class="py">getContextClassLoader</span>
<span class="k">val</span> <span class="nv">userJarUrl</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">userJar</span><span class="o">).</span><span class="py">toURI</span><span class="o">().</span><span class="py">toURL</span><span class="o">()</span>
<span class="k">val</span> <span class="nv">loader</span> <span class="k">=</span>
<span class="nf">if</span> <span class="o">(</span><span class="nv">sys</span><span class="o">.</span><span class="py">props</span><span class="o">.</span><span class="py">getOrElse</span><span class="o">(</span><span class="s">"spark.driver.userClassPathFirst"</span><span class="o">,</span> <span class="s">"false"</span><span class="o">).</span><span class="py">toBoolean</span><span class="o">)</span> <span class="o">{</span>
<span class="k">new</span> <span class="nc">ChildFirstURLClassLoader</span><span class="o">(</span><span class="nc">Array</span><span class="o">(</span><span class="n">userJarUrl</span><span class="o">),</span> <span class="n">currentLoader</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="k">new</span> <span class="nc">MutableURLClassLoader</span><span class="o">(</span><span class="nc">Array</span><span class="o">(</span><span class="n">userJarUrl</span><span class="o">),</span> <span class="n">currentLoader</span><span class="o">)</span>
<span class="o">}</span>
<span class="cm">/*
* 此时通过反射从userJarURL获取用户入口代码,调用用户的入口程序,然后执行. 在初始化SparkContext的时候会把spark.jars
* 所指定的所有jar都添加到集群中 为将来执行tasks准备好依赖环境, return c.newInstance()
*/</span>
<span class="nv">Thread</span><span class="o">.</span><span class="py">currentThread</span><span class="o">.</span><span class="py">setContextClassLoader</span><span class="o">(</span><span class="n">loader</span><span class="o">)</span>
<span class="nf">setupDependencies</span><span class="o">(</span><span class="n">loader</span><span class="o">,</span> <span class="n">userJar</span><span class="o">)</span>
<span class="c1">// Delegate to supplied main class</span>
<span class="k">val</span> <span class="nv">clazz</span> <span class="k">=</span> <span class="nv">Utils</span><span class="o">.</span><span class="py">classForName</span><span class="o">(</span><span class="n">mainClass</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">mainMethod</span> <span class="k">=</span> <span class="nv">clazz</span><span class="o">.</span><span class="py">getMethod</span><span class="o">(</span><span class="s">"main"</span><span class="o">,</span> <span class="n">classOf</span><span class="o">[</span><span class="kt">Array</span><span class="o">[</span><span class="kt">String</span><span class="o">]])</span>
<span class="nv">mainMethod</span><span class="o">.</span><span class="py">invoke</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="nv">extraArgs</span><span class="o">.</span><span class="py">toArray</span><span class="o">[</span><span class="kt">String</span><span class="o">])</span>
<span class="nv">rpcEnv</span><span class="o">.</span><span class="py">shutdown</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>现在<code class="language-plaintext highlighter-rouge">Driver</code>已经启动了,接下来看应用如何启动<code class="language-plaintext highlighter-rouge">executor</code>和<code class="language-plaintext highlighter-rouge">task</code>的流程,<code class="language-plaintext highlighter-rouge">Executor</code>的启动从<code class="language-plaintext highlighter-rouge">SparkContext#createTaskScheduler(SparkContext, String, String)</code>方法,方法体中会初始化<code class="language-plaintext highlighter-rouge">StandaloneSchedulerBackend</code>类。<code class="language-plaintext highlighter-rouge">SparkContext</code>准备完成后会调用<code class="language-plaintext highlighter-rouge">_taskScheduler.start()</code>方法启动<code class="language-plaintext highlighter-rouge">StandaloneSchedulerBackend</code>方法:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's</span>
<span class="c1">// constructor (YarnSchedule)</span>
<span class="nv">_taskScheduler</span><span class="o">.</span><span class="py">start</span><span class="o">()</span>
<span class="k">private</span> <span class="k">def</span> <span class="nf">createTaskScheduler</span><span class="o">(</span>
<span class="n">sc</span><span class="k">:</span> <span class="kt">SparkContext</span><span class="o">,</span>
<span class="n">master</span><span class="k">:</span> <span class="kt">String</span><span class="o">,</span>
<span class="n">deployMode</span><span class="k">:</span> <span class="kt">String</span><span class="o">)</span><span class="k">:</span> <span class="o">(</span><span class="kt">SchedulerBackend</span><span class="o">,</span> <span class="kt">TaskScheduler</span><span class="o">)</span> <span class="k">=</span> <span class="o">{</span>
<span class="k">case</span> <span class="nc">SPARK_REGEX</span><span class="o">(</span><span class="n">sparkUrl</span><span class="o">)</span> <span class="k">=></span>
<span class="k">val</span> <span class="nv">scheduler</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">TaskSchedulerImpl</span><span class="o">(</span><span class="n">sc</span><span class="o">)</span> <span class="cm">/* standalone模式下执行任务调度器executor */</span>
<span class="k">val</span> <span class="nv">masterUrls</span> <span class="k">=</span> <span class="nv">sparkUrl</span><span class="o">.</span><span class="py">split</span><span class="o">(</span><span class="s">","</span><span class="o">).</span><span class="py">map</span><span class="o">(</span><span class="s">"spark://"</span> <span class="o">+</span> <span class="k">_</span><span class="o">)</span>
<span class="cm">/* 重点, 用户程序向master注册, executor申请都是由该函数完成的. start是在TaskSchedulerImpl中的start函数里启动的 */</span>
<span class="k">val</span> <span class="nv">backend</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">StandaloneSchedulerBackend</span><span class="o">(</span><span class="n">scheduler</span><span class="o">,</span> <span class="n">sc</span><span class="o">,</span> <span class="n">masterUrls</span><span class="o">)</span>
<span class="nv">scheduler</span><span class="o">.</span><span class="py">initialize</span><span class="o">(</span><span class="n">backend</span><span class="o">)</span>
<span class="o">(</span><span class="n">backend</span><span class="o">,</span> <span class="n">scheduler</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p>进入<code class="language-plaintext highlighter-rouge">StandaloneSchedulerBackend#start()</code>方法,用<code class="language-plaintext highlighter-rouge">CoarseGrainedExecutorBackend</code>构建<code class="language-plaintext highlighter-rouge">command</code>命令,然后构建<code class="language-plaintext highlighter-rouge">ApplicationDescription</code>对象,将其传入<code class="language-plaintext highlighter-rouge">appClient</code>并向<code class="language-plaintext highlighter-rouge">Master</code>发起应用注册的请求<code class="language-plaintext highlighter-rouge">StandaloneAppClient#tryRegisterAllMasters()</code>方法中发送<code class="language-plaintext highlighter-rouge">RegisterApplication(appDescription, self)</code>,<code class="language-plaintext highlighter-rouge">Master</code>端收到请求后会重新运行<code class="language-plaintext highlighter-rouge">schedule()</code>的方法。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">start</span><span class="o">()</span> <span class="o">{</span>
<span class="nv">super</span><span class="o">.</span><span class="py">start</span><span class="o">()</span>
<span class="c1">// Start executors with a few necessary configs for registering with the scheduler</span>
<span class="cm">/* 只获取了Executor启动时用到的配置,不包含--jars传递的值 */</span>
<span class="k">val</span> <span class="nv">sparkJavaOpts</span> <span class="k">=</span> <span class="nv">Utils</span><span class="o">.</span><span class="py">sparkJavaOpts</span><span class="o">(</span><span class="n">conf</span><span class="o">,</span> <span class="nv">SparkConf</span><span class="o">.</span><span class="py">isExecutorStartupConf</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">javaOpts</span> <span class="k">=</span> <span class="n">sparkJavaOpts</span> <span class="o">++</span> <span class="n">extraJavaOpts</span>
<span class="k">val</span> <span class="nv">command</span> <span class="k">=</span> <span class="nc">Command</span><span class="o">(</span><span class="s">"org.apache.spark.executor.CoarseGrainedExecutorBackend"</span><span class="o">,</span>
<span class="n">args</span><span class="o">,</span> <span class="nv">sc</span><span class="o">.</span><span class="py">executorEnvs</span><span class="o">,</span> <span class="n">classPathEntries</span> <span class="o">++</span> <span class="n">testingClassPath</span><span class="o">,</span> <span class="n">libraryPathEntries</span><span class="o">,</span> <span class="n">javaOpts</span><span class="o">)</span>
<span class="c1">// 重点关注两个参数 spark.executor.extraLibraryPath spark.driver.extraLibraryPath</span>
<span class="k">val</span> <span class="nv">webUrl</span> <span class="k">=</span> <span class="nv">sc</span><span class="o">.</span><span class="py">ui</span><span class="o">.</span><span class="py">map</span><span class="o">(</span><span class="nv">_</span><span class="o">.</span><span class="py">webUrl</span><span class="o">).</span><span class="py">getOrElse</span><span class="o">(</span><span class="s">""</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">coresPerExecutor</span> <span class="k">=</span> <span class="nv">conf</span><span class="o">.</span><span class="py">getOption</span><span class="o">(</span><span class="s">"spark.executor.cores"</span><span class="o">).</span><span class="py">map</span><span class="o">(</span><span class="nv">_</span><span class="o">.</span><span class="py">toInt</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">appDesc</span> <span class="k">=</span> <span class="nc">ApplicationDescription</span><span class="o">(</span><span class="nv">sc</span><span class="o">.</span><span class="py">appName</span><span class="o">,</span> <span class="n">maxCores</span><span class="o">,</span> <span class="nv">sc</span><span class="o">.</span><span class="py">executorMemory</span><span class="o">,</span> <span class="n">command</span><span class="o">,</span>
<span class="n">webUrl</span><span class="o">,</span> <span class="nv">sc</span><span class="o">.</span><span class="py">eventLogDir</span><span class="o">,</span> <span class="nv">sc</span><span class="o">.</span><span class="py">eventLogCodec</span><span class="o">,</span> <span class="n">coresPerExecutor</span><span class="o">,</span> <span class="n">initialExecutorLimit</span><span class="o">)</span>
<span class="n">client</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">StandaloneAppClient</span><span class="o">(</span><span class="nv">sc</span><span class="o">.</span><span class="py">env</span><span class="o">.</span><span class="py">rpcEnv</span><span class="o">,</span> <span class="n">masters</span><span class="o">,</span> <span class="n">appDesc</span><span class="o">,</span> <span class="k">this</span><span class="o">,</span> <span class="n">conf</span><span class="o">)</span>
<span class="nv">client</span><span class="o">.</span><span class="py">start</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>进入<code class="language-plaintext highlighter-rouge">Worker#receive()</code>方法,根据<code class="language-plaintext highlighter-rouge">case</code>匹配到<code class="language-plaintext highlighter-rouge">LaunchExecutor</code>的请求,构建<code class="language-plaintext highlighter-rouge">ExecutorRunner</code>对象并调用其<code class="language-plaintext highlighter-rouge">start()</code>方法。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="nc">LaunchExecutor</span><span class="o">(</span><span class="n">masterUrl</span><span class="o">,</span> <span class="n">appId</span><span class="o">,</span> <span class="n">execId</span><span class="o">,</span> <span class="n">appDesc</span><span class="o">,</span> <span class="n">cores_</span><span class="o">,</span> <span class="n">memory_</span><span class="o">)</span> <span class="k">=></span>
<span class="nf">if</span> <span class="o">(</span><span class="n">masterUrl</span> <span class="o">!=</span> <span class="n">activeMasterUrl</span><span class="o">)</span> <span class="o">{</span>
<span class="nf">logWarning</span><span class="o">(</span><span class="s">"Invalid Master ("</span> <span class="o">+</span> <span class="n">masterUrl</span> <span class="o">+</span> <span class="s">") attempted to launch executor."</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="s">"Asked to launch executor %s/%d for %s"</span><span class="o">.</span><span class="py">format</span><span class="o">(</span><span class="n">appId</span><span class="o">,</span> <span class="n">execId</span><span class="o">,</span> <span class="nv">appDesc</span><span class="o">.</span><span class="py">name</span><span class="o">))</span>
<span class="k">val</span> <span class="nv">manager</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">ExecutorRunner</span><span class="o">(</span>
<span class="n">appId</span><span class="o">,</span>
<span class="n">execId</span><span class="o">,</span>
<span class="nv">appDesc</span><span class="o">.</span><span class="py">copy</span><span class="o">(</span><span class="n">command</span> <span class="k">=</span> <span class="nv">Worker</span><span class="o">.</span><span class="py">maybeUpdateSSLSettings</span><span class="o">(</span><span class="nv">appDesc</span><span class="o">.</span><span class="py">command</span><span class="o">,</span> <span class="n">conf</span><span class="o">)),</span>
<span class="n">cores_</span><span class="o">,</span>
<span class="n">memory_</span><span class="o">,</span>
<span class="n">self</span><span class="o">,</span>
<span class="n">workerId</span><span class="o">,</span>
<span class="n">host</span><span class="o">,</span>
<span class="nv">webUi</span><span class="o">.</span><span class="py">boundPort</span><span class="o">,</span>
<span class="n">publicAddress</span><span class="o">,</span>
<span class="n">sparkHome</span><span class="o">,</span>
<span class="n">executorDir</span><span class="o">,</span>
<span class="n">workerUri</span><span class="o">,</span>
<span class="n">conf</span><span class="o">,</span>
<span class="n">appLocalDirs</span><span class="o">,</span> <span class="nv">ExecutorState</span><span class="o">.</span><span class="py">RUNNING</span><span class="o">)</span>
<span class="nf">executors</span><span class="o">(</span><span class="n">appId</span> <span class="o">+</span> <span class="s">"/"</span> <span class="o">+</span> <span class="n">execId</span><span class="o">)</span> <span class="k">=</span> <span class="n">manager</span>
<span class="nv">manager</span><span class="o">.</span><span class="py">start</span><span class="o">()</span>
<span class="n">coresUsed</span> <span class="o">+=</span> <span class="n">cores_</span>
<span class="n">memoryUsed</span> <span class="o">+=</span> <span class="n">memory_</span>
<span class="nf">sendToMaster</span><span class="o">(</span><span class="nc">ExecutorStateChanged</span><span class="o">(</span><span class="n">appId</span><span class="o">,</span> <span class="n">execId</span><span class="o">,</span> <span class="nv">manager</span><span class="o">.</span><span class="py">state</span><span class="o">,</span> <span class="nc">None</span><span class="o">,</span> <span class="nc">None</span><span class="o">))</span>
<span class="o">}</span>
</code></pre></div></div>
<p>进入<code class="language-plaintext highlighter-rouge">ExecutorRunner#start()</code>方法,首先创建了一个<code class="language-plaintext highlighter-rouge">worker</code>线程用于执行任务,要执行的方法为<code class="language-plaintext highlighter-rouge">fetchAndRunExecutor()</code>。在方法中通过<code class="language-plaintext highlighter-rouge">CommandUtils.buildProcessBuilder()</code>创建进程,然后设置执行路径、环境变量以及<code class="language-plaintext highlighter-rouge">spark UI</code>相关内容,然后启动进程(<code class="language-plaintext highlighter-rouge">process</code>执行类为<code class="language-plaintext highlighter-rouge">CoarseGrainedExecutorBackend</code>)。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Download and run the executor described in our ApplicationDescription
*/</span>
<span class="k">private</span> <span class="k">def</span> <span class="nf">fetchAndRunExecutor</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// Launch the process</span>
<span class="k">val</span> <span class="nv">subsOpts</span> <span class="k">=</span> <span class="nv">appDesc</span><span class="o">.</span><span class="py">command</span><span class="o">.</span><span class="py">javaOpts</span><span class="o">.</span><span class="py">map</span> <span class="o">{</span>
<span class="nv">Utils</span><span class="o">.</span><span class="py">substituteAppNExecIds</span><span class="o">(</span><span class="k">_</span><span class="o">,</span> <span class="n">appId</span><span class="o">,</span> <span class="nv">execId</span><span class="o">.</span><span class="py">toString</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">val</span> <span class="nv">subsCommand</span> <span class="k">=</span> <span class="nv">appDesc</span><span class="o">.</span><span class="py">command</span><span class="o">.</span><span class="py">copy</span><span class="o">(</span><span class="n">javaOpts</span> <span class="k">=</span> <span class="n">subsOpts</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">builder</span> <span class="k">=</span> <span class="nv">CommandUtils</span><span class="o">.</span><span class="py">buildProcessBuilder</span><span class="o">(</span><span class="n">subsCommand</span><span class="o">,</span> <span class="k">new</span> <span class="nc">SecurityManager</span><span class="o">(</span><span class="n">conf</span><span class="o">),</span>
<span class="n">memory</span><span class="o">,</span> <span class="nv">sparkHome</span><span class="o">.</span><span class="py">getAbsolutePath</span><span class="o">,</span> <span class="n">substituteVariables</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">command</span> <span class="k">=</span> <span class="nv">builder</span><span class="o">.</span><span class="py">command</span><span class="o">()</span>
<span class="k">val</span> <span class="nv">formattedCommand</span> <span class="k">=</span> <span class="nv">command</span><span class="o">.</span><span class="py">asScala</span><span class="o">.</span><span class="py">mkString</span><span class="o">(</span><span class="s">"\""</span><span class="o">,</span> <span class="s">"\" \""</span><span class="o">,</span> <span class="s">"\""</span><span class="o">)</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="n">s</span><span class="s">"Launch command: $formattedCommand"</span><span class="o">)</span>
<span class="c1">// 执行构建完成的ProcessBuilder</span>
<span class="n">process</span> <span class="k">=</span> <span class="nv">builder</span><span class="o">.</span><span class="py">start</span><span class="o">()</span>
<span class="k">val</span> <span class="nv">header</span> <span class="k">=</span> <span class="s">"Spark Executor Command: %s\n%s\n\n"</span><span class="o">.</span><span class="py">format</span><span class="o">(</span>
<span class="n">formattedCommand</span><span class="o">,</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">40</span><span class="o">)</span>
<span class="c1">// Wait for it to exit; executor may exit with code 0 (when driver instructs it to shutdown)</span>
<span class="c1">// or with nonzero exit code</span>
<span class="k">val</span> <span class="nv">exitCode</span> <span class="k">=</span> <span class="nv">process</span><span class="o">.</span><span class="py">waitFor</span><span class="o">()</span>
<span class="n">state</span> <span class="k">=</span> <span class="nv">ExecutorState</span><span class="o">.</span><span class="py">EXITED</span>
<span class="k">val</span> <span class="nv">message</span> <span class="k">=</span> <span class="s">"Command exited with code "</span> <span class="o">+</span> <span class="n">exitCode</span>
<span class="nv">worker</span><span class="o">.</span><span class="py">send</span><span class="o">(</span><span class="nc">ExecutorStateChanged</span><span class="o">(</span><span class="n">appId</span><span class="o">,</span> <span class="n">execId</span><span class="o">,</span> <span class="n">state</span><span class="o">,</span> <span class="nc">Some</span><span class="o">(</span><span class="n">message</span><span class="o">),</span> <span class="nc">Some</span><span class="o">(</span><span class="n">exitCode</span><span class="o">)))</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">CoarseGrainedExecutorBackend#receive()</code>方法中接收<code class="language-plaintext highlighter-rouge">case LaunchTask(data)</code>的请求,当<code class="language-plaintext highlighter-rouge">executor</code>初始化好之后执行<code class="language-plaintext highlighter-rouge">executor.launchTask(this, taskDesc)</code>方法。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">receive</span><span class="k">:</span> <span class="kt">PartialFunction</span><span class="o">[</span><span class="kt">Any</span>, <span class="kt">Unit</span><span class="o">]</span> <span class="k">=</span> <span class="o">{</span>
<span class="k">case</span> <span class="nc">LaunchTask</span><span class="o">(</span><span class="n">data</span><span class="o">)</span> <span class="k">=></span>
<span class="nf">if</span> <span class="o">(</span><span class="n">executor</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nf">exitExecutor</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="s">"Received LaunchTask command but executor was null"</span><span class="o">)</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">taskDesc</span> <span class="k">=</span> <span class="nv">TaskDescription</span><span class="o">.</span><span class="py">decode</span><span class="o">(</span><span class="nv">data</span><span class="o">.</span><span class="py">value</span><span class="o">)</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="s">"Got assigned task "</span> <span class="o">+</span> <span class="nv">taskDesc</span><span class="o">.</span><span class="py">taskId</span><span class="o">)</span>
<span class="nv">executor</span><span class="o">.</span><span class="py">launchTask</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">taskDesc</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>进入<code class="language-plaintext highlighter-rouge">TaskRunner#run()</code>方法,设置<code class="language-plaintext highlighter-rouge">TaskMemoryManager</code>、序列化<code class="language-plaintext highlighter-rouge">jar</code>文件、初始化各种<code class="language-plaintext highlighter-rouge">Metrics</code>统计信息,然后通过<code class="language-plaintext highlighter-rouge">task.run()</code>的任务就正常执行了。至此,从使用<code class="language-plaintext highlighter-rouge">spark-submit.sh</code>脚本提交用户<code class="language-plaintext highlighter-rouge">application</code>在<code class="language-plaintext highlighter-rouge">standalone</code>模式下的流程就先分析完成。</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">def</span> <span class="nf">run</span><span class="o">()</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">taskMemoryManager</span> <span class="k">=</span> <span class="k">new</span> <span class="nc">TaskMemoryManager</span><span class="o">(</span><span class="nv">env</span><span class="o">.</span><span class="py">memoryManager</span><span class="o">,</span> <span class="n">taskId</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">deserializeStartTime</span> <span class="k">=</span> <span class="nv">System</span><span class="o">.</span><span class="py">currentTimeMillis</span><span class="o">()</span>
<span class="k">val</span> <span class="nv">deserializeStartCpuTime</span> <span class="k">=</span> <span class="nf">if</span> <span class="o">(</span><span class="nv">threadMXBean</span><span class="o">.</span><span class="py">isCurrentThreadCpuTimeSupported</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">threadMXBean</span><span class="o">.</span><span class="py">getCurrentThreadCpuTime</span>
<span class="o">}</span> <span class="k">else</span> <span class="mi">0L</span>
<span class="cm">/*
* 类加载器设置的是url类加载器, 而其父类加载器是系统类加载器. currentJars是以来的uri, 用户在调用
* updateDependencies将依赖添加至此
*/</span>
<span class="nv">Thread</span><span class="o">.</span><span class="py">currentThread</span><span class="o">.</span><span class="py">setContextClassLoader</span><span class="o">(</span><span class="n">replClassLoader</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">ser</span> <span class="k">=</span> <span class="nv">env</span><span class="o">.</span><span class="py">closureSerializer</span><span class="o">.</span><span class="py">newInstance</span><span class="o">()</span>
<span class="nf">logInfo</span><span class="o">(</span><span class="n">s</span><span class="s">"Running $taskName (TID $taskId)"</span><span class="o">)</span>
<span class="c1">// Run the actual task and measure its runtime.</span>
<span class="n">taskStartTime</span> <span class="k">=</span> <span class="nv">System</span><span class="o">.</span><span class="py">currentTimeMillis</span><span class="o">()</span>
<span class="n">taskStartCpu</span> <span class="k">=</span> <span class="nf">if</span> <span class="o">(</span><span class="nv">threadMXBean</span><span class="o">.</span><span class="py">isCurrentThreadCpuTimeSupported</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">threadMXBean</span><span class="o">.</span><span class="py">getCurrentThreadCpuTime</span>
<span class="o">}</span> <span class="k">else</span> <span class="mi">0L</span>
<span class="k">var</span> <span class="n">threwException</span> <span class="k">=</span> <span class="kc">true</span>
<span class="k">val</span> <span class="nv">value</span> <span class="k">=</span> <span class="nv">Utils</span><span class="o">.</span><span class="py">tryWithSafeFinally</span> <span class="o">{</span>
<span class="k">val</span> <span class="nv">res</span> <span class="k">=</span> <span class="nv">task</span><span class="o">.</span><span class="py">run</span><span class="o">(</span>
<span class="n">taskAttemptId</span> <span class="k">=</span> <span class="n">taskId</span><span class="o">,</span>
<span class="n">attemptNumber</span> <span class="k">=</span> <span class="nv">taskDescription</span><span class="o">.</span><span class="py">attemptNumber</span><span class="o">,</span>
<span class="n">metricsSystem</span> <span class="k">=</span> <span class="nv">env</span><span class="o">.</span><span class="py">metricsSystem</span><span class="o">)</span>
<span class="n">threwException</span> <span class="k">=</span> <span class="kc">false</span>
<span class="n">res</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
jvm常见垃圾回收算法及双亲委派模型
2021-02-10T00:00:00+00:00
https://dongma.github.io/2021/02/10/jvm-garbage-collect-algorithm
<p><code class="language-plaintext highlighter-rouge">java</code>相对于<code class="language-plaintext highlighter-rouge">C++</code>优势在于自动的垃圾回收,提供对象的构造函数后,不需要再提供析构函数(销毁对象,释放之前申请的内存),更易避免了内存泄露的问题。主要归功于虚拟机进行垃圾回收,虚拟机版本有<code class="language-plaintext highlighter-rouge">Sun</code>公司的<code class="language-plaintext highlighter-rouge">HotSopt VM</code>、<code class="language-plaintext highlighter-rouge">BEA</code>的<code class="language-plaintext highlighter-rouge">JRockit</code>、微软的<code class="language-plaintext highlighter-rouge">JVM</code>及<code class="language-plaintext highlighter-rouge">IBM</code>的<code class="language-plaintext highlighter-rouge">J9 VM</code>。</p>
<h3 id="内存区域划分">内存区域划分</h3>
<p><code class="language-plaintext highlighter-rouge">Java</code>虚拟机在执行程序时会把管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间。</p>
<p>1)程序计数器(<code class="language-plaintext highlighter-rouge">Program Counter Register</code>)占用一块较小的内存空间,可看作是当前线程执行字节码的行号指示器(与操作系统中的<code class="language-plaintext highlighter-rouge">PC</code>的概念相同,指定下一条指令的位置)。在执行分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。每个线程都有一个独立的程序计数器;</p>
<p>2)<code class="language-plaintext highlighter-rouge">Java</code>虚拟机栈也是线程私有的,声明周期与线程相同。描述<code class="language-plaintext highlighter-rouge">Java</code>方法执行的内存模型,每个方法执行会创建一个栈帧 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。递归方法调用超过最大深度时 将跑出<code class="language-plaintext highlighter-rouge">StackOverflowError</code>的异常;
<!-- more -->
3)本地方法栈(<code class="language-plaintext highlighter-rouge">Native Method Stack</code>)用于调用其它语言的方法(如<code class="language-plaintext highlighter-rouge">C++</code>),声明周期也与线程绑定;</p>
<p>4)<code class="language-plaintext highlighter-rouge">Java</code>堆是多个线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,也是垃圾收集器管理的主要区域。由于收集器基本采用分代收集算法,<code class="language-plaintext highlighter-rouge">Java</code>堆还可以细分为:新生代和老年代(细致些有<code class="language-plaintext highlighter-rouge">Eden</code>空间、<code class="language-plaintext highlighter-rouge">From Survivor</code>空间、<code class="language-plaintext highlighter-rouge">To Survivor</code>空间)等;</p>
<p>5)方法区(<code class="language-plaintext highlighter-rouge">Method Area</code>)与<code class="language-plaintext highlighter-rouge">Java</code>堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后代码等数据。在<code class="language-plaintext highlighter-rouge">HotSpot</code>中,开发者更愿意把方法去称为“永久代”(<code class="language-plaintext highlighter-rouge">Permanent Generation</code>),但本质上并不等价;</p>
<p>6)运行时常量池(<code class="language-plaintext highlighter-rouge">Runtime Constant Pool</code>)和直接内存(<code class="language-plaintext highlighter-rouge">Direct Memory</code>)这两部分,用于存储<code class="language-plaintext highlighter-rouge">class</code>文件翻译出来的直接引用也存储在运行时常量池中,直接内存主要用于<code class="language-plaintext highlighter-rouge">NIO</code>类;</p>
<h3 id="回收算法及垃圾收集器">回收算法及垃圾收集器</h3>
<p>如何判断一个对象已死?已有引用计数算法,但其无法解决循环引用的问题。<code class="language-plaintext highlighter-rouge">java</code>采用可达性分析算法,算法的基本思路 是通过一系列称为”<code class="language-plaintext highlighter-rouge">gc roots</code> “的对象作为起始点,搜索所走过的路径称为引用链(<code class="language-plaintext highlighter-rouge">reference chain</code>),当<code class="language-plaintext highlighter-rouge">gc</code>不可达时 则证明此对象是不可用的。</p>
<p>“标记-清除”(<code class="language-plaintext highlighter-rouge">Mark-Sweep</code>)算法,首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。问题在于,一个是效率问题 标记和清除两个过程的效率都不高,另外,还会产生大量不连续的内存碎片。在分配较大对象时,容易产生<code class="language-plaintext highlighter-rouge">OOM</code>。</p>
<p>为了解决效率问题,一种称为复制(<code class="language-plaintext highlighter-rouge">copying</code>)的算法出现了,它将可用内存按容量划分为大小相等的两个块。每次将存活的对象复制到另一个块。但是,内存利用率不高 存在<code class="language-plaintext highlighter-rouge">50%</code>的内存浪费。目前商业虚拟机分为<code class="language-plaintext highlighter-rouge">1</code>个<code class="language-plaintext highlighter-rouge">80%</code>的<code class="language-plaintext highlighter-rouge">Edge</code>区和<code class="language-plaintext highlighter-rouge">2</code>个<code class="language-plaintext highlighter-rouge">10%</code>的<code class="language-plaintext highlighter-rouge">Survivor</code>区。</p>
<p>根据老年代的特点,有人提出了另外一种“标记-整理”(<code class="language-plaintext highlighter-rouge">Mark Compact</code>)算法,差异在于不清理可回收的对象,而是让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。</p>
<p>商业上目前用的是分代回收(<code class="language-plaintext highlighter-rouge">Generational Collect</code>)算法,根据对象存活周期的不同将内存划分为几块。一般是把<code class="language-plaintext highlighter-rouge">Java</code>堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。</p>
<p><strong>常见的垃圾回收器</strong>,垃圾回收器就是内存回收的具体实现,<code class="language-plaintext highlighter-rouge">Java</code>虚拟机规范中对实现没有任何规定,因此,不同厂商虚拟机使用的垃圾收集器可能会有很大差别。</p>
<p><strong><code class="language-plaintext highlighter-rouge">Serial</code>收集器</strong>,是发展历史最悠久的收集器,曾经(在<code class="language-plaintext highlighter-rouge">JDK 1.3.1</code>之前)是虚拟机新生代收集的唯一选择。该收集器会引入”<code class="language-plaintext highlighter-rouge">stop the world</code>“的问题,进行垃圾收集时必须暂停其它所有的工作线程,直到它结束。</p>
<p><strong><code class="language-plaintext highlighter-rouge">ParNew</code>收集器</strong>,是<code class="language-plaintext highlighter-rouge">Serial</code>收集器的多线程版本,除了使用多条线程进行垃圾收集外,却是许多运行在<code class="language-plaintext highlighter-rouge">Server</code>模式下的虚拟机首选新生代收集器,只有<code class="language-plaintext highlighter-rouge">ParNew</code>和<code class="language-plaintext highlighter-rouge">Serial</code>能够与<code class="language-plaintext highlighter-rouge">CMS</code>收集器配合工作。</p>
<p><strong><code class="language-plaintext highlighter-rouge">Parallel Scavenge</code>收集器</strong>是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。其优点在于达到一个可控制的吞吐量(<code class="language-plaintext highlighter-rouge">Throughput</code>),吞吐量就是<code class="language-plaintext highlighter-rouge">CPU</code>用于运行用户代码的时间与<code class="language-plaintext highlighter-rouge">CPU</code>总耗时间的比值。</p>
<p><strong><code class="language-plaintext highlighter-rouge">CMS</code>收集器</strong>是一种以获取最短回收停顿时间为目标的收集器,其是基于“标记-清除”算法实现的,整个过程分为:初始标记(<code class="language-plaintext highlighter-rouge">initial mark</code>)、并发标记(<code class="language-plaintext highlighter-rouge">concurrent mark</code>)、重新标记(<code class="language-plaintext highlighter-rouge">remark</code>)、并发清除(<code class="language-plaintext highlighter-rouge">concurrent sweep</code>)。耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,停顿的时间较少。</p>
<p><code class="language-plaintext highlighter-rouge">CMS</code>是基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,当没有足够空间来满足大对象分配时,那不得不提前进行一个<code class="language-plaintext highlighter-rouge">Full GC</code>。<code class="language-plaintext highlighter-rouge">CMS</code>收集器提供了<code class="language-plaintext highlighter-rouge">+UseCMSCompactAtFullCollection</code>开关参数,用于在<code class="language-plaintext highlighter-rouge">CMS</code>收集器进行<code class="language-plaintext highlighter-rouge">Full GC</code>时合并内存碎片。</p>
<p>此外,<code class="language-plaintext highlighter-rouge">CMS</code>收集器对<code class="language-plaintext highlighter-rouge">CPU</code>资源是非常敏感的,在并发阶段,虽不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢。</p>
<p><strong><code class="language-plaintext highlighter-rouge">G1</code>收集器</strong>是当今收集器技术发展的最前沿成果之一,由<code class="language-plaintext highlighter-rouge">Java 1.7</code>引入。<code class="language-plaintext highlighter-rouge">G1</code>是一款面向服务端应用的垃圾收集器,<code class="language-plaintext highlighter-rouge">HotSpot</code>开发团队赋予它的使命是在未来可以替换掉<code class="language-plaintext highlighter-rouge">JDK 1.5</code>中发布的<code class="language-plaintext highlighter-rouge">CMS</code>收集器。与其它<code class="language-plaintext highlighter-rouge">GC</code>收集器相比,<code class="language-plaintext highlighter-rouge">G1</code>具备以下特点:</p>
<p>并发与并行,<code class="language-plaintext highlighter-rouge">G1</code>能充分利用多<code class="language-plaintext highlighter-rouge">CPU</code>、多核环境下的硬件优势,使用多个<code class="language-plaintext highlighter-rouge">CPU</code>来缩短<code class="language-plaintext highlighter-rouge">Stop The World</code>的停顿时间;分代收集,分代的概念在<code class="language-plaintext highlighter-rouge">G1</code>中依然得以保留。它可以采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过了多次<code class="language-plaintext highlighter-rouge">GC</code>的旧对象以获取更好的收集效果;</p>
<p>空间整合,与<code class="language-plaintext highlighter-rouge">CMS</code>的“标记-整理”算法不同,<code class="language-plaintext highlighter-rouge">G1</code>从整体来看是基于“标记-整理”算法实现的收集器,从局部上来看是基于“复制”算法实现的;可预测的停顿,这个<code class="language-plaintext highlighter-rouge">G1</code>相对于<code class="language-plaintext highlighter-rouge">CMS</code>的另一大优势,降低停顿时间是<code class="language-plaintext highlighter-rouge">G1</code>和<code class="language-plaintext highlighter-rouge">CMS</code>共同的关注点,但<code class="language-plaintext highlighter-rouge">G1</code>处理追求低停顿外,还能建立可预测的停顿时间。</p>
<h3 id="类加载器与双亲委派模型">类加载器与双亲委派模型</h3>
<p>从<code class="language-plaintext highlighter-rouge">Java</code>虚拟机的角度来讲,只存在两种不同的类加载器:一种是自动类加载器(<code class="language-plaintext highlighter-rouge">Bootstrap ClassLoader</code>),这个类加载器使用<code class="language-plaintext highlighter-rouge">C++</code>实现。另一种就是所有其它的类加载器,包括扩展类加载器(<code class="language-plaintext highlighter-rouge">Extension ClassLoader</code>)、应用程序类加载器(<code class="language-plaintext highlighter-rouge">Application ClassLoader</code>),及一些实现了<code class="language-plaintext highlighter-rouge">ClassLoader</code>接口的自定义类加载器。</p>
<p>双亲委派模型对于保证<code class="language-plaintext highlighter-rouge">Java</code>程序的稳定运作很重要,但其实现却非常简单,只需实现<code class="language-plaintext highlighter-rouge">java.lang.ClassLoader</code>的<code class="language-plaintext highlighter-rouge">loadClass()</code>方法就可以(同时设置<code class="language-plaintext highlighter-rouge">class</code>文件的<code class="language-plaintext highlighter-rouge">path</code>)。加载逻辑:先检查是否已被加载过,若没有加载则调用父加载器的<code class="language-plaintext highlighter-rouge">loadClass</code>方法,若父加载器为空则默认使用启动类加载器作为父加载器。</p>
<p>破坏双亲委派模型,覆写<code class="language-plaintext highlighter-rouge">loadClass()</code>方法实现加载<code class="language-plaintext highlighter-rouge">class</code>的逻辑,而类加载器和抽象类<code class="language-plaintext highlighter-rouge">java.lang.ClassLoader</code>在<code class="language-plaintext highlighter-rouge">JDK 1.0</code>时代就已经存在。在<code class="language-plaintext highlighter-rouge">JDK 1.2</code>之后已不提倡用户再去覆盖<code class="language-plaintext highlighter-rouge">loadClass()</code>方法,而应当把自己的类加载逻辑写到<code class="language-plaintext highlighter-rouge">findClass()</code>方法中。</p>
<p>双亲委派模型的第二次破坏是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题。可在线程上下文类加载器(<code class="language-plaintext highlighter-rouge">Thread Context ClassLoader</code>)中通过<code class="language-plaintext highlighter-rouge">Thread</code>的<code class="language-plaintext highlighter-rouge">setContextClassLoader()</code>进行设置。若创建线程时还未设置,它将会从父线程中继承一个,默认为应用程序类加载器,也算是一种“舞弊”的方式。第三次破坏是由于用户追求动态性的追求导致的,这里的“动态性”指的是当前一个非常“热门”的名次:代码热替换(<code class="language-plaintext highlighter-rouge">HotSwap</code>)、模块热部署(<code class="language-plaintext highlighter-rouge">Hot Deployment</code>)等,采用<code class="language-plaintext highlighter-rouge">OSGI</code>的技术。</p>
使用kubernetes构建微服务
2019-11-12T00:00:00+00:00
https://dongma.github.io/2019/11/12/kubernetes-deployment
<p><img src="https://raw.githubusercontent.com/dongma/springcloud/master/kubernetes/images/kubernetes_logo.png" alt="kubernetes" />
<!-- more --></p>
<h2 id="build-distributed-services-with-kubernetes">Build distributed services with kubernetes</h2>
<blockquote>
<p><strong>Kubernetes</strong> (commonly stylized as k8s) is an open-source container-orchestration system for automating application deployment, scaling, and management. It aims to provide a “platform for automating deployment, scaling, and operations of application containers across clusters of hosts”.</p>
</blockquote>
<h4 id="一在elementory-os服务器搭建kubernetes环境">一、在<code class="language-plaintext highlighter-rouge">elementory OS</code>服务器搭建kubernetes环境</h4>
<p><code class="language-plaintext highlighter-rouge">elementary OS</code>是基于<code class="language-plaintext highlighter-rouge">ubuntu</code>精心打磨美化的桌面 <code class="language-plaintext highlighter-rouge">linux</code> 发行版的一款软件,号称 “最美的 <code class="language-plaintext highlighter-rouge">linux</code>”, 最早是 <code class="language-plaintext highlighter-rouge">ubuntu</code> 的一个美化主题项目,现在成了独立的发行版。”快速、开源、注重隐私的 <code class="language-plaintext highlighter-rouge">windows</code> /<code class="language-plaintext highlighter-rouge"> macOS</code> 替代品”。</p>
<p>1)在<code class="language-plaintext highlighter-rouge">elementary OS</code>系统上安装<code class="language-plaintext highlighter-rouge">docker</code>环境,具体可以参考<code class="language-plaintext highlighter-rouge"> https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/</code>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1.更新ubuntu的apt源索引</span>
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>apt-get update
<span class="c"># 2.安装以下包以使apt可以通过HTTPS使用存储库repository</span>
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>apt-get <span class="nb">install </span>apt-transport-https ca-certificates curl software-properties-common
<span class="c"># 3.添加Docker官方GPG key</span>
sam@elementoryos:~<span class="nv">$ </span>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>apt-key add -
<span class="c"># 4.设置Docker稳定版仓库</span>
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>add-apt-repository <span class="s2">"deb [arch=amd64] https://download.docker.com/linux/ubuntu </span><span class="si">$(</span>lsb_release <span class="nt">-cs</span><span class="si">)</span><span class="s2"> stable"</span>
<span class="c"># 5.再更新下apt源索引,然后通过docker version显示器版本信息</span>
sam@elementoryos:~<span class="nv">$ </span>apt-get update
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>docker version
Client:
Version: 18.09.7
Server:
Engine:
Version: 18.09.7
<span class="c"># 6.从镜像中心拉取hello-world镜像并进行运行</span>
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
</code></pre></div></div>
<p>管理<code class="language-plaintext highlighter-rouge">docker</code>服务常用应用脚本:</p>
<p>` sudo service docker start ` 启动<code class="language-plaintext highlighter-rouge">docker</code>服务、<code class="language-plaintext highlighter-rouge"> sudo service docker stop </code> 停止<code class="language-plaintext highlighter-rouge">docker</code>服务、<code class="language-plaintext highlighter-rouge"> sudo service docker restart </code>重启docker服务.</p>
<p>2)使用<code class="language-plaintext highlighter-rouge">minikube</code>在本机搭建<code class="language-plaintext highlighter-rouge">kubernetes</code>集群,简单体验<code class="language-plaintext highlighter-rouge">k8s</code>:</p>
<p>为了方便开发者开发和体验<code class="language-plaintext highlighter-rouge">kubernetes</code>,社区提供了可以在本地部署的<code class="language-plaintext highlighter-rouge">minikube</code>。由于国内网络的限制导致,导致在本地安装<code class="language-plaintext highlighter-rouge">minikube</code>时相关的依赖是无法下载。从<code class="language-plaintext highlighter-rouge">minikube</code>最新的<code class="language-plaintext highlighter-rouge">1.5</code>版本之后,已经提供了配置化的方式,可以直接从阿里云的镜像地址来获取所需要的<code class="language-plaintext highlighter-rouge">docker</code>镜像和配置。</p>
<p>在<code class="language-plaintext highlighter-rouge">elementary OS</code>上安装<code class="language-plaintext highlighter-rouge">kubectl</code>的稳定版本:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>curl <span class="nt">-LO</span> https://storage.googleapis.com/kubernetes-release/release/v1.16.0/bin/linux/amd64/kubectl <span class="o">&&</span> <span class="nb">chmod</span> +x ./kubectl <span class="o">&&</span> <span class="nb">sudo mv</span> ./kubectl /usr/local/bin/kubectl
</code></pre></div></div>
<p>在安装完成后使用<code class="language-plaintext highlighter-rouge">kubectl version</code>进行验证,由于<code class="language-plaintext highlighter-rouge">minikube</code>服务未启动最后的报错可以忽略:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl version
Client Version: version.Info<span class="o">{</span>Major:<span class="s2">"1"</span>, Minor:<span class="s2">"16"</span>, GitVersion:<span class="s2">"v1.16.0"</span>, GitCommit:<span class="s2">"2bd9643cee5b3b3a5ecbd3af49d09018f0773c77"</span>, GitTreeState:<span class="s2">"clean"</span>, BuildDate:<span class="s2">"2019-09-18T14:36:53Z"</span>, GoVersion:<span class="s2">"go1.12.9"</span>, Compiler:<span class="s2">"gc"</span>, Platform:<span class="s2">"linux/amd64"</span><span class="o">}</span>
The connection to the server 192.168.170.130:8443 was refused - did you specify the right host or port?
</code></pre></div></div>
<p>通过<code class="language-plaintext highlighter-rouge">curl</code>命令从<code class="language-plaintext highlighter-rouge">github</code>上下载<code class="language-plaintext highlighter-rouge">minikube</code>的<code class="language-plaintext highlighter-rouge">1.5.0</code>版本:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span>curl <span class="nt">-Lo</span> minikube https://github.com/kubernetes/minikube/releases/download/v1.5.0/minikube-linux-amd64 <span class="o">&&</span> <span class="nb">chmod</span> +x minikube <span class="o">&&</span> <span class="nb">sudo mv </span>minikube /usr/local/bin/
</code></pre></div></div>
<p>启动<code class="language-plaintext highlighter-rouge">minikube</code>服务,为了访问海外资源阿里云提供了一系列基础措施可以通过参数进行配置,<code class="language-plaintext highlighter-rouge">--image-mirror-country cn</code>默认会从<code class="language-plaintext highlighter-rouge">registry.cn-hangzhou.aliyuncs.com/google_containers</code>下载<code class="language-plaintext highlighter-rouge">kubernetes</code>依赖的相关资源。首次启动会在本地下载<code class="language-plaintext highlighter-rouge"> localkube </code>、<code class="language-plaintext highlighter-rouge">kubeadm</code>等工具。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>minikube start <span class="nt">--vm-driver</span><span class="o">=</span>none <span class="nt">--image-mirror-country</span> cn <span class="nt">--memory</span><span class="o">=</span>1024mb <span class="nt">--disk-size</span><span class="o">=</span>8192mb <span class="nt">--registry-mirror</span><span class="o">=</span>https://registry.docker-cn.com <span class="nt">--image-repository</span><span class="o">=</span><span class="s1">'registry.cn-hangzhou.aliyuncs.com/google_containers'</span> <span class="nt">--bootstrapper</span><span class="o">=</span>kubeadm <span class="nt">--extra-config</span><span class="o">=</span>apiserver.authorization-mode<span class="o">=</span>RBAC
😄 minikube v1.5.0 on Debian buster/sid
✅ Using image repository registry.cn-hangzhou.aliyuncs.com/google_containers
🤹 Running on localhost <span class="o">(</span><span class="nv">CPUs</span><span class="o">=</span>2, <span class="nv">Memory</span><span class="o">=</span>3653MB, <span class="nv">Disk</span><span class="o">=</span>40059MB<span class="o">)</span> ...
ℹ️ OS release is elementary OS 5.0 Juno
🐳 Preparing Kubernetes v1.16.2 on Docker 18.09.7 ...
🏄 Done! kubectl is now configured to use <span class="s2">"minikube"</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">minikube</code>安装完成后,在本地<code class="language-plaintext highlighter-rouge">minikube dashboard --url</code>控制页面无法展示,目前暂时未解决。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl create clusterrolebinding add-on-cluster-admin <span class="nt">--clusterrole</span><span class="o">=</span>cluster-admin <span class="nt">--serviceaccount</span><span class="o">=</span>kube-system:default
</code></pre></div></div>
<p>使用<code class="language-plaintext highlighter-rouge">sudo minikube dashboard --url</code>自动生成<code class="language-plaintext highlighter-rouge">minikube</code>的管理页面:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~$ sudo minikube dashboard -url
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">minikube</code>本地环境搭建可参考这几篇文章:</p>
<p>使用<code class="language-plaintext highlighter-rouge">minikube</code>在本地搭建集群:http://qii404.me/2018/01/06/minukube.html</p>
<p>阿里云的<code class="language-plaintext highlighter-rouge">minikube</code>本地实验环境:https://yq.aliyun.com/articles/221687</p>
<p>关于<code class="language-plaintext highlighter-rouge">kubernetes</code>解决<code class="language-plaintext highlighter-rouge">dashboard</code>:https://blog.8hfq.com/2019/03/01/kubernetes-dashboard.html</p>
<h4 id="二运行于kubernetes中的容器">二、运行于kubernetes中的容器</h4>
<p><code class="language-plaintext highlighter-rouge">kubernetes</code>中的<code class="language-plaintext highlighter-rouge">pod</code>组件:<code class="language-plaintext highlighter-rouge">pod</code>是一组并置的容器,代表了<code class="language-plaintext highlighter-rouge">kubernetes</code>中基本构建模块。在实际应用中我们并不会单独部署容器,更多的是针对一组<code class="language-plaintext highlighter-rouge">pod</code>容器进行部署和操作。当一个<code class="language-plaintext highlighter-rouge">pod</code>包含多个容器时,这些容器总是会运行于同一个工作节点上——一个<code class="language-plaintext highlighter-rouge">pod</code>绝不会跨越多个工作节点。</p>
<p>对于<code class="language-plaintext highlighter-rouge">docker</code>和<code class="language-plaintext highlighter-rouge">kubernetes</code>期望的工作方式是将每个进程运行于自己的容器内,由于不能将多个进程聚集在一个单独的容器中,我们需要另一种更高级的结构来将容器绑定在一起,并将它们作为一个单元进行管理,这就是<code class="language-plaintext highlighter-rouge">pod</code>背后的根本原理。对于容器彼此之间是完全隔离的,但此时我们期望的是隔离容器组,而不是单个容器,并让容器组内的容器共享一些资源。<code class="language-plaintext highlighter-rouge">kubernetes</code>通过配置<code class="language-plaintext highlighter-rouge">docker</code>来让一个<code class="language-plaintext highlighter-rouge">pod</code>内的所有容器共享相同的<code class="language-plaintext highlighter-rouge">linux</code>命名空间,而不是每个容器都有自己的一组命名空间。</p>
<p>由于一个<code class="language-plaintext highlighter-rouge">pod</code>中的容器运行于相同的<code class="language-plaintext highlighter-rouge">network</code>命名空间中,因此它们共享相同的<code class="language-plaintext highlighter-rouge">IP</code>地址和端口空间。这意味着在同一<code class="language-plaintext highlighter-rouge">pod</code>中的容器运行的多个进程需要注意不能绑定想同的端口号,否则会导致端口冲突。</p>
<p>1)在<code class="language-plaintext highlighter-rouge">kubernetes</code>上运行第一个应用<code class="language-plaintext highlighter-rouge">swagger-editor</code>并对外暴露<code class="language-plaintext highlighter-rouge">8081</code>端口:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl run swagger-editor <span class="nt">--image</span><span class="o">=</span>swaggerapi/swagger-editor:latest <span class="nt">--port</span><span class="o">=</span>8081 <span class="nt">--generator</span><span class="o">=</span>run/v1
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods
NAME READY STATUS RESTARTS AGE
swagger-editor-xgqzm 1/1 Running 0 57s
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">kubectl run</code>命令中使用<code class="language-plaintext highlighter-rouge">--generator=run/v1</code>参数表示它让<code class="language-plaintext highlighter-rouge">kubernetes</code>创建一个<code class="language-plaintext highlighter-rouge">ReplicationController</code>而不是<code class="language-plaintext highlighter-rouge">Deployment</code>。通过<code class="language-plaintext highlighter-rouge">kubectl get pods</code>可以查看所有<code class="language-plaintext highlighter-rouge">pod</code>中运行的容器实例信息。每个<code class="language-plaintext highlighter-rouge">pod</code>都有自己的<code class="language-plaintext highlighter-rouge">ip</code>地址,但是这个地址是集群内部的,只有通过<code class="language-plaintext highlighter-rouge">LoadBalancer</code>类型服务公开它,才可以被外部访问,可以通过运行<code class="language-plaintext highlighter-rouge">kubectl get services</code>命令查看新创建的服务对象。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl expose rc swagger-editor <span class="nt">--type</span><span class="o">=</span>LoadBalancer <span class="nt">--name</span> swagger-editor-http
service/swagger-editor-http exposed
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT<span class="o">(</span>S<span class="o">)</span> AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 46m
swagger-editor-http LoadBalancer 10.108.118.211 <pending> 8081:30507/TCP 3m24s
</code></pre></div></div>
<p>2)为了增加期望的副本数,需要改变<code class="language-plaintext highlighter-rouge">ReplicationController</code>期望的副本数,现已告诉<code class="language-plaintext highlighter-rouge">kubernetes</code>需要采取行动,对<code class="language-plaintext highlighter-rouge">pod</code>的数量采取操作来实现期望的状态。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl scale rc swagger-editor <span class="nt">--replicas</span><span class="o">=</span>3
replicationcontroller/swagger-editor scaled
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods
NAME READY STATUS RESTARTS AGE
swagger-editor-fzppq 0/1 ContainerCreating 0 12s
swagger-editor-wqpg5 0/1 ContainerCreating 0 12s
swagger-editor-xgqzm 1/1 Running 0 16m
</code></pre></div></div>
<p>为了观察列出<code class="language-plaintext highlighter-rouge">pod</code>时显示<code class="language-plaintext highlighter-rouge">pod ip</code>和<code class="language-plaintext highlighter-rouge">pod</code>的节点,可以通过使用<code class="language-plaintext highlighter-rouge">-o wide</code>选项请求显示其他列。在列出<code class="language-plaintext highlighter-rouge">pod</code>时,该选项显示<code class="language-plaintext highlighter-rouge">pod</code>的<code class="language-plaintext highlighter-rouge">ip</code>和所运行的节点。由于<code class="language-plaintext highlighter-rouge">minikube</code>不支持<code class="language-plaintext highlighter-rouge">rc</code>,因而并不会展示外部<code class="language-plaintext highlighter-rouge">ip</code>地址。若想在不通过<code class="language-plaintext highlighter-rouge">service</code>的情况下与某个特定的<code class="language-plaintext highlighter-rouge">pod</code>进行通信(处于调试或其它原因),<code class="language-plaintext highlighter-rouge">kubernetes</code>将允许我们配置端口转发到该<code class="language-plaintext highlighter-rouge">pod</code>,可以通过<code class="language-plaintext highlighter-rouge">kubectl port-forward</code>命令完成上述操作:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods <span class="nt">-o</span> wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
swagger-editor-fzppq 1/1 Running 0 5m28s 172.17.0.7 minikube <none> <none>
swagger-editor-wqpg5 1/1 Running 0 5m28s 172.17.0.5 minikube <none> <none>
swagger-editor-xgqzm 1/1 Running 0 21m 172.17.0.6 minikube <none> <none>
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl port-forward swagger-editor-fzppq 8088:8081
Forwarding from 127.0.0.1:8088 -> 8081
Forwarding from <span class="o">[</span>::1]:8088 -> 8081
</code></pre></div></div>
<p>标签是一种简单却功能强大的<code class="language-plaintext highlighter-rouge">kubernetes</code>特性,不仅可以组织<code class="language-plaintext highlighter-rouge">pod</code>也可以组织所有其他的<code class="language-plaintext highlighter-rouge">kubernetes</code>资源。详细来讲,可以通过标签选择器来筛选<code class="language-plaintext highlighter-rouge">pod</code>资源。在使用多个<code class="language-plaintext highlighter-rouge">namespace</code>的前提下,我们可以将包括大量组件的复杂系统拆分为更小的不同组,这些不同组也可以在多租户环境中分配资源。</p>
<h4 id="三副本机制和其它控制器部署托管的pod">三、副本机制和其它控制器:部署托管的<code class="language-plaintext highlighter-rouge">pod</code></h4>
<p><code class="language-plaintext highlighter-rouge">kubernetes</code>可以通过存活探针<code class="language-plaintext highlighter-rouge">(liveness probe)</code>检查容器是否还在运行,可以为<code class="language-plaintext highlighter-rouge">pod</code>中的每个容器单独指定存活探针。如果探测失败,<code class="language-plaintext highlighter-rouge">kubernetes</code>将定期执行探针并重新启动容器。<code class="language-plaintext highlighter-rouge">kubernetes</code>有三种探测容器的机制:通过<code class="language-plaintext highlighter-rouge">http get</code>对容器发送请求,若应用接收到请求,并且响应状态码不代表错误,则任务探测成功;<code class="language-plaintext highlighter-rouge">TCP</code>套接字探针尝试与容器指定端口建立<code class="language-plaintext highlighter-rouge">TCP</code>连接,若长连接正常建立则探测成功;<code class="language-plaintext highlighter-rouge">exec</code>探针在容器中执行任意命令,并检查命令的退出返回码。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kubia-liveness</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">image</span><span class="pi">:</span> <span class="s">luksa/kubia-unhealthy</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kubia</span>
<span class="na">livenessProbe</span><span class="pi">:</span>
<span class="na">httpGet</span><span class="pi">:</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
<span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
<span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">15</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">kubia-liveness-probe-initial-delay.yaml</code>文件中在<code class="language-plaintext highlighter-rouge">livenessProbe</code>中指定了通过<code class="language-plaintext highlighter-rouge">httpGet</code>探测的探针地址检测应用的状态,为了防止容器启动时通过探针地址检测应用状态,可以通过设置<code class="language-plaintext highlighter-rouge">initialDelaySeconds</code>指定应用启动间隔时间(像<code class="language-plaintext highlighter-rouge">spingboot</code>应用的<code class="language-plaintext highlighter-rouge">/health</code>端点就非常合适)。</p>
<p>了解<code class="language-plaintext highlighter-rouge">ReplicationController</code>组件:<code class="language-plaintext highlighter-rouge">ReplicationController</code>是一种<code class="language-plaintext highlighter-rouge">kubernetes</code>资源,可确保它的<code class="language-plaintext highlighter-rouge">pod</code>始终保持运行状态。如果<code class="language-plaintext highlighter-rouge">pod</code>因任何原因消失,则<code class="language-plaintext highlighter-rouge">ReplicationController</code>会注意到缺少了<code class="language-plaintext highlighter-rouge">pod</code>并创建替代<code class="language-plaintext highlighter-rouge">pod</code>。<code class="language-plaintext highlighter-rouge">ReplicationController</code>的工作是确保<code class="language-plaintext highlighter-rouge">pod</code>的数量始终与其标签选择器匹配,若不匹配则<code class="language-plaintext highlighter-rouge">rc</code>会根据需要,采取适当的操作来协调<code class="language-plaintext highlighter-rouge">pod</code>的数量。<code class="language-plaintext highlighter-rouge">label selector</code>用于确定<code class="language-plaintext highlighter-rouge">rc</code>作用域内有哪些<code class="language-plaintext highlighter-rouge">pod</code>、<code class="language-plaintext highlighter-rouge">replica count</code>指定应运行的<code class="language-plaintext highlighter-rouge">pod</code>数量、<code class="language-plaintext highlighter-rouge">pod template</code>用于创建新的<code class="language-plaintext highlighter-rouge">pod</code>副本。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ReplicationController</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kubia</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">kubia</span>
<span class="na">template</span><span class="pi">:</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">kubia</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">kubia</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">luksa/kubia</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">kubia-rc.yaml</code>文件定义,在<code class="language-plaintext highlighter-rouge">yaml</code>中<code class="language-plaintext highlighter-rouge">selector</code>指定了符合标签的选择器<code class="language-plaintext highlighter-rouge">app: kubia</code>。若删除的<code class="language-plaintext highlighter-rouge">rc</code>创建的一个<code class="language-plaintext highlighter-rouge">pod</code>,则其会自动创建新的<code class="language-plaintext highlighter-rouge">pod</code>使得副本的数量达到<code class="language-plaintext highlighter-rouge">yaml</code>文件配置的数量。若要将<code class="language-plaintext highlighter-rouge">pod</code>移出<code class="language-plaintext highlighter-rouge">rc</code>作用域,可以通过更改<code class="language-plaintext highlighter-rouge">pod</code>的标签将其从<code class="language-plaintext highlighter-rouge">rc</code>的作用域中进行移除,<code class="language-plaintext highlighter-rouge">--overwrite</code>参数是必要的,否则<code class="language-plaintext highlighter-rouge">kubectl</code>将只是打印出警告,并不会更改标签。对于修改<code class="language-plaintext highlighter-rouge">rc</code>的<code class="language-plaintext highlighter-rouge">template</code>只会对之后新创建的<code class="language-plaintext highlighter-rouge">pod</code>有影响,而对之前已有的<code class="language-plaintext highlighter-rouge">pod</code>不会造成影响。若需要对<code class="language-plaintext highlighter-rouge">pod</code>进行水平扩展,可以通过修改<code class="language-plaintext highlighter-rouge">edit</code>调整<code class="language-plaintext highlighter-rouge">replicas:10</code>的属性,或者通过命令行<code class="language-plaintext highlighter-rouge">kubectl scale rc kubia --replication=10</code>进行调整。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl create <span class="nt">-f</span> kubia-rc.yaml
ReplicationController <span class="s2">"kubia"</span> created
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl label pod kubia-demdck <span class="nv">app</span><span class="o">=</span>foo <span class="nt">--overwrite</span>
<span class="c"># 通过kubectl更改rc的template内容</span>
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl edit rc kubia
</code></pre></div></div>
<p>当要删除<code class="language-plaintext highlighter-rouge">rc</code>则可以通过<code class="language-plaintext highlighter-rouge">kubectl delete</code>进行操作,<code class="language-plaintext highlighter-rouge">rc</code>所管理的所有<code class="language-plaintext highlighter-rouge">pod</code>也会被删除。若需要保留<code class="language-plaintext highlighter-rouge">pod</code>的时候,则需要在命令行添加<code class="language-plaintext highlighter-rouge">--cascade=false</code>的配置,当删除<code class="language-plaintext highlighter-rouge">replicationController</code>后,其之前所管理的<code class="language-plaintext highlighter-rouge">pod</code>就独立。</p>
<p><code class="language-plaintext highlighter-rouge">ReplicaSet</code>的引入:最初<code class="language-plaintext highlighter-rouge">ReplicationController</code>是用于复制和在异常时重新调度节点的唯一<code class="language-plaintext highlighter-rouge">kubernetes</code>组件,后来引入了<code class="language-plaintext highlighter-rouge">ReplicaSet</code>的类似资源。它是新一代的<code class="language-plaintext highlighter-rouge">rc</code>并且会将其完全替换掉。<code class="language-plaintext highlighter-rouge">ReplicaSet</code>的行为与<code class="language-plaintext highlighter-rouge">rc</code>完全相同,但<code class="language-plaintext highlighter-rouge">pod</code>选择器的表达能力更强。在<code class="language-plaintext highlighter-rouge">yaml</code>文件配置中其<code class="language-plaintext highlighter-rouge">apiVersion</code>内容为<code class="language-plaintext highlighter-rouge">apps/v1beta2</code>,其<code class="language-plaintext highlighter-rouge">kind</code>类型为<code class="language-plaintext highlighter-rouge">ReplicaSet</code>类型。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl delete rs kubia
</code></pre></div></div>
<p>引入<code class="language-plaintext highlighter-rouge">DaemonSet</code>组件:要在所有集群结点上运行一个<code class="language-plaintext highlighter-rouge">pod</code>,需要创建一个<code class="language-plaintext highlighter-rouge">DaemonSet</code>对象。<code class="language-plaintext highlighter-rouge">DaemonSet</code>确保创建足够的<code class="language-plaintext highlighter-rouge">pod</code>,并在自己的节点上部署每个<code class="language-plaintext highlighter-rouge">pod</code>。尽管<code class="language-plaintext highlighter-rouge">ReplicaSet(ReplicationController)</code>确保集群中存在期望数量的<code class="language-plaintext highlighter-rouge">pod</code>副本,但<code class="language-plaintext highlighter-rouge">DaemonSet</code>并没有期望的副本的概念。它不需要,因为它的工作是确保一个<code class="language-plaintext highlighter-rouge">pod</code>匹配它的选择器并在每个节点上运行。</p>
<p>在<code class="language-plaintext highlighter-rouge">DaemonSet</code>的<code class="language-plaintext highlighter-rouge">yml</code>配置文件中,其<code class="language-plaintext highlighter-rouge">apiVersion</code>内容为<code class="language-plaintext highlighter-rouge">apps/v1beta2</code>,<code class="language-plaintext highlighter-rouge">kind</code>类型为<code class="language-plaintext highlighter-rouge">DeamonSet</code>。在删除<code class="language-plaintext highlighter-rouge">DaemonSet</code>时候其所管理<code class="language-plaintext highlighter-rouge">pod</code>也会被一并删除。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl create <span class="nt">-d</span> ssd-monitor-deamonset.yaml
<span class="c"># view all DaemonSet components in kubernetes</span>
sam@elementoryos:~<span class="nv">$ </span><span class="nb">sudo </span>kubectl get ds
</code></pre></div></div>
<p>介绍<code class="language-plaintext highlighter-rouge">Kubernetes Job</code>资源:<code class="language-plaintext highlighter-rouge">kubernetes</code>通过<code class="language-plaintext highlighter-rouge">Job</code>资源提供对短任务的支持,在发生节点故障时,该节点上由<code class="language-plaintext highlighter-rouge">Job</code>管理的<code class="language-plaintext highlighter-rouge">pod</code>将按照<code class="language-plaintext highlighter-rouge">ReplicaSet</code>的<code class="language-plaintext highlighter-rouge">pod</code>的方式,重新安排到其他节点。如果进程本身异常退出(进程返回错误退出代码时),可以将<code class="language-plaintext highlighter-rouge">Job</code>配置为重新启动容器。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">batch/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Job</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">batch-job</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">completions</span><span class="pi">:</span> <span class="m">5</span>
<span class="na">parallelism</span><span class="pi">:</span> <span class="m">2</span>
<span class="na">schedule</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0,15,30,45</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*"</span>
<span class="na">template</span><span class="pi">:</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">batch-job</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">OnFailure</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">main</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">luksa/batch-job</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Job</code>是<code class="language-plaintext highlighter-rouge">batch API</code>组<code class="language-plaintext highlighter-rouge">v1</code>版本的一部分,<code class="language-plaintext highlighter-rouge">yaml</code>定义了一个<code class="language-plaintext highlighter-rouge">Job</code>类型的资源,它将运行<code class="language-plaintext highlighter-rouge">luksa/batch-job</code>镜像,该镜像调用一个运行<code class="language-plaintext highlighter-rouge">120</code>秒的进程,然后退出。在<code class="language-plaintext highlighter-rouge">pod</code>的定义中,可以指定在容器中运行的进程结束时,<code class="language-plaintext highlighter-rouge">kubernetes</code>会做什么?这是通过<code class="language-plaintext highlighter-rouge">pod</code>配置的属性<code class="language-plaintext highlighter-rouge">restartPolicy</code>完成的,默认为<code class="language-plaintext highlighter-rouge">Always</code>配置 在<code class="language-plaintext highlighter-rouge">Job</code>中使用<code class="language-plaintext highlighter-rouge">OnFailure</code>的策略。可以在<code class="language-plaintext highlighter-rouge">yaml</code>文件中指定<code class="language-plaintext highlighter-rouge">parallelism: 2</code>来指定任务的并行度,通过创建<code class="language-plaintext highlighter-rouge">cronJob</code>资源在<code class="language-plaintext highlighter-rouge">yaml</code>中指定‘<code class="language-plaintext highlighter-rouge">schedule: 0,15,30,45 * * * *</code>定时任务表达式。<code class="language-plaintext highlighter-rouge">startingDeadlineSeconds: 15</code>指定<code class="language-plaintext highlighter-rouge">pod</code>最迟必须在预定时间后<code class="language-plaintext highlighter-rouge">15</code>秒开始执行。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl create <span class="nt">-f</span> kubernetes-job.yaml
job.batch/batch-job created
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get <span class="nb">jobs
</span>NAME COMPLETIONS DURATION AGE
batch-job 0/1 47s 47s
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods
NAME READY STATUS RESTARTS AGE
batch-job-nzbmv 1/1 Running 0 108s
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl logs batch-job-nzbmv
Sun Nov 17 09:09:01 UTC 2019 Batch job starting
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">service</code>服务:让客户端发现<code class="language-plaintext highlighter-rouge">pod</code>并与之通信</p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">kubernetes</code>服务是一种为一组功能相同<code class="language-plaintext highlighter-rouge">pod</code>提供单一不变的接入点的资源,当服务存在时,它的<code class="language-plaintext highlighter-rouge">ip</code>地址和端口不变。客户端通过固定<code class="language-plaintext highlighter-rouge">ip</code>和<code class="language-plaintext highlighter-rouge">port</code>建立连接,这种连接会被路由到提供该服务的任意一个<code class="language-plaintext highlighter-rouge">pod</code>上。通过这种方式,客户端不需要知道每个<code class="language-plaintext highlighter-rouge">pod</code>的地址,这样这些<code class="language-plaintext highlighter-rouge">pod</code>就可以在集群中被随时创建或者移除。</p>
</blockquote>
<p>可以使用<code class="language-plaintext highlighter-rouge">kubectl expose</code>命令创建服务,<code class="language-plaintext highlighter-rouge">rc</code>是<code class="language-plaintext highlighter-rouge">replicationcontroller</code>的缩写。由于<code class="language-plaintext highlighter-rouge">minikube</code>不支持<code class="language-plaintext highlighter-rouge">LoadBalance</code>类型的服务,因此服务的<code class="language-plaintext highlighter-rouge">external-ip</code>地址为<code class="language-plaintext highlighter-rouge"><none></code>。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl expose rc kubia <span class="nt">--type</span><span class="o">=</span>LoadBalancer <span class="nt">--name</span> kubia-http
service <span class="s2">"kubia-http"</span> exposed
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT<span class="o">(</span>S<span class="o">)</span> AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d5h
kubia ClusterIP 10.111.211.203 <none> 80/TCP,443/TCP 22h
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods
NAME READY STATUS RESTARTS AGE
kubia-9vds6 1/1 Running 0 23h
kubia-cpjvx 1/1 Running 0 23h
kubia-hs5vq 1/1 Running 0 23h
</code></pre></div></div>
<p>另一种是使用<code class="language-plaintext highlighter-rouge">yaml</code>描述文件<code class="language-plaintext highlighter-rouge">kubia-svc.yaml</code>来创建服务,使用<code class="language-plaintext highlighter-rouge">sudo kubectl create -f kubia-svc.yaml </code> 。<code class="language-plaintext highlighter-rouge">service</code>也是通过<code class="language-plaintext highlighter-rouge">selector</code>筛选符合条件的<code class="language-plaintext highlighter-rouge">pod</code>,通过<code class="language-plaintext highlighter-rouge">ports</code>对端口进行转发。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kubia</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
<span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">kubia</span>
</code></pre></div></div>
<p>从内部集群测试服务,可以通过<code class="language-plaintext highlighter-rouge">kubectl exec</code>命令在一个已经存在的<code class="language-plaintext highlighter-rouge">pod</code>中执行<code class="language-plaintext highlighter-rouge">curl</code>命令,其作用和<code class="language-plaintext highlighter-rouge">docker exec</code>命令比较类似。在<code class="language-plaintext highlighter-rouge">kubernetes</code>命令中<code class="language-plaintext highlighter-rouge">--</code>代表着<code class="language-plaintext highlighter-rouge">kubectl</code>命令项的结束,在<code class="language-plaintext highlighter-rouge">--</code>后的内容是在<code class="language-plaintext highlighter-rouge">pod</code>内部需要执行的命令。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl <span class="nb">exec </span>kubia-9vds6 <span class="nt">--</span> curl <span class="nt">-s</span> http://10.111.211.203
You<span class="s1">'ve hit kubia-cpjvx
</span></code></pre></div></div>
<p>通过环境变量发现服务:在<code class="language-plaintext highlighter-rouge">pod</code>开始的时候,<code class="language-plaintext highlighter-rouge">kubernetes</code>会初始化一系列的环境变量指向现在存在的服务。一旦选择了目标<code class="language-plaintext highlighter-rouge">pod</code>,通过在容器中运行<code class="language-plaintext highlighter-rouge">env</code>来列出所有的环境变量。在<code class="language-plaintext highlighter-rouge">ENV</code>列出的环境变量中,<code class="language-plaintext highlighter-rouge">KUBIA_SERVICE_HOST</code>和<code class="language-plaintext highlighter-rouge">KUBIA_SERVICE_PORT</code>分表代表了<code class="language-plaintext highlighter-rouge">kubia</code>服务的<code class="language-plaintext highlighter-rouge">ip</code>地址和端口号。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl <span class="nb">exec </span>kubia-9vds6 <span class="nb">env
</span><span class="nv">PATH</span><span class="o">=</span>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
<span class="nv">HOSTNAME</span><span class="o">=</span>kubia-9vds6
<span class="nv">KUBERNETES_PORT_443_TCP_PORT</span><span class="o">=</span>443
<span class="nv">KUBERNETES_PORT_443_TCP_ADDR</span><span class="o">=</span>10.96.0.1
<span class="nv">KUBERNETES_SERVICE_HOST</span><span class="o">=</span>10.96.0.1
<span class="nv">KUBERNETES_SERVICE_PORT</span><span class="o">=</span>443
<span class="nv">KUBERNETES_SERVICE_PORT_HTTPS</span><span class="o">=</span>443
<span class="nv">KUBERNETES_PORT</span><span class="o">=</span>tcp://10.96.0.1:443
<span class="nv">KUBERNETES_PORT_443_TCP</span><span class="o">=</span>tcp://10.96.0.1:443
<span class="nv">KUBERNETES_PORT_443_TCP_PROTO</span><span class="o">=</span>tcp
<span class="nv">NPM_CONFIG_LOGLEVEL</span><span class="o">=</span>info
<span class="nv">NODE_VERSION</span><span class="o">=</span>7.9.0
<span class="nv">YARN_VERSION</span><span class="o">=</span>0.22.0
<span class="nv">HOME</span><span class="o">=</span>/root
</code></pre></div></div>
<p>通过<code class="language-plaintext highlighter-rouge">dns</code>发现服务:在<code class="language-plaintext highlighter-rouge">kube-system</code>命名空间下列出的所有<code class="language-plaintext highlighter-rouge">pod</code>信息,其中一个为<code class="language-plaintext highlighter-rouge">coredns-755587fdc8</code>。每个服务从内部<code class="language-plaintext highlighter-rouge">dns</code>服务器中获得一个<code class="language-plaintext highlighter-rouge">dns</code>条目,客户端的<code class="language-plaintext highlighter-rouge">pod</code>在知道服务名称的情况下可以通过全限定域名<code class="language-plaintext highlighter-rouge">(FQDN)</code>来访问,而不是诉诸于环境变量。前端<code class="language-plaintext highlighter-rouge">pod</code>可以通过<code class="language-plaintext highlighter-rouge">backend-database.default.svc.cluster.local</code>访问后端数据库服务:<code class="language-plaintext highlighter-rouge">backend-database</code>对应于服务名称,<code class="language-plaintext highlighter-rouge">default</code>表示服务在其中定义的名称空间,<code class="language-plaintext highlighter-rouge">svc.cluster.local</code>是在所有集群本地服务名称中使用的可配置集群域后缀。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods <span class="nt">--namespace</span> kube-system
NAME READY STATUS RESTARTS AGE
coredns-755587fdc8-nz7s8 0/1 CrashLoopBackOff 80 2d6h
etcd-minikube 1/1 Running 0 2d6h
kube-addon-manager-minikube 1/1 Running 0 2d6h
kube-apiserver-minikube 1/1 Running 0 2d6h
kube-controller-manager-minikube 1/1 Running 0 2d6h
kube-proxy-gczr4 1/1 Running 0 2d6h
kube-scheduler-minikube 1/1 Running 0 2d6h
storage-provisioner 1/1 Running 0 2d6h
</code></pre></div></div>
<p>由于<code class="language-plaintext highlighter-rouge">kubernetes</code>容器编排中<code class="language-plaintext highlighter-rouge">kube-dns</code>服务不可用,因而在<code class="language-plaintext highlighter-rouge">pod</code>内部无法实现通过<code class="language-plaintext highlighter-rouge">service.namespace.clustername</code>访问<code class="language-plaintext highlighter-rouge">exposed</code>服务。在<code class="language-plaintext highlighter-rouge">pod</code>内部<code class="language-plaintext highlighter-rouge">/etc/resolv.conf</code>文件中保存内容与<code class="language-plaintext highlighter-rouge">host</code>文件类似。在<code class="language-plaintext highlighter-rouge">curl</code>这个服务是工作的,但却是<code class="language-plaintext highlighter-rouge">ping</code>不通的,因为服务的集群<code class="language-plaintext highlighter-rouge">ip</code>是一个虚拟<code class="language-plaintext highlighter-rouge">ip</code>,并且只有在于服务端口结合时才有意义。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl <span class="nb">exec</span> <span class="nt">-it</span> kubia-9vds6 bash
<span class="o">[</span><span class="nb">sudo</span><span class="o">]</span> password <span class="k">for </span>sam: <span class="k">******</span>
root@kubia-9vds6:/# curl http://kubia.default.svc.cluster.local
curl: <span class="o">(</span>6<span class="o">)</span> Could not resolve host: kubia.default.svc.cluster.local
root@kubia-9vds6:/# curl http://kubia.default
curl: <span class="o">(</span>6<span class="o">)</span> Could not resolve host: kubia.default
root@kubia-9vds6:/# curl http://kubia
curl: <span class="o">(</span>6<span class="o">)</span> Could not resolve host: kubia
root@kubia-9vds6:/# <span class="nb">cat</span> /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local localdomain
</code></pre></div></div>
<p>连接集群外部的服务:在<code class="language-plaintext highlighter-rouge">kubernetes</code>中,服务并不是和<code class="language-plaintext highlighter-rouge">pod</code>直接相连的。相反,有一种资源介于两者之前——它就是<code class="language-plaintext highlighter-rouge">Endpoint</code>资源。如果之前在服务在运行过<code class="language-plaintext highlighter-rouge">kubectl describe</code>。<code class="language-plaintext highlighter-rouge">endpoint</code>资源就是暴露一个服务的<code class="language-plaintext highlighter-rouge">ip</code>地址和端口的列表,<code class="language-plaintext highlighter-rouge">endpoint</code>资源和其他<code class="language-plaintext highlighter-rouge">kubernetes</code>资源一样,所以可以使用<code class="language-plaintext highlighter-rouge">kubectl info</code>来获取它的基本信息。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl describe svc kubia
<span class="o">[</span><span class="nb">sudo</span><span class="o">]</span> password <span class="k">for </span>sam:
Name: kubia
Namespace: default
Labels: <none>
Annotations: <none>
Selector: <span class="nv">app</span><span class="o">=</span>kubia
Type: ClusterIP
IP: 10.111.211.203
Port: http 80/TCP
TargetPort: 8080/TCP
Endpoints: 172.17.0.5:8080,172.17.0.6:8080,172.17.0.7:8080
Port: https 443/TCP
TargetPort: 8443/TCP
Endpoints: 172.17.0.5:8443,172.17.0.6:8443,172.17.0.7:8443
Session Affinity: ClientIP
Events: <none>
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get endpoints kubia
NAME ENDPOINTS AGE
kubia 172.17.0.5:8443,172.17.0.6:8443,172.17.0.7:8443 + 3 more... 23h
</code></pre></div></div>
<p>将服务暴露给外部客户端:服务的<code class="language-plaintext highlighter-rouge">pod</code>不仅可以在<code class="language-plaintext highlighter-rouge">kubernetes</code>内部进行调用,有时,<code class="language-plaintext highlighter-rouge">k8s</code>还需要向外部服务公开某些服务(例如<code class="language-plaintext highlighter-rouge">web</code>服务器,以便外部客户端可以访问它们)。</p>
<blockquote>
<p>有几种方式可以在外部访问服务:将服务类型设置为<code class="language-plaintext highlighter-rouge">NodePort</code>——每个集群节点都会在节点上打开一个端口,对于<code class="language-plaintext highlighter-rouge">NodePort</code>服务,每个集群节点在节点本身上打开一个端口,并将该端口上接收到的流量重定向到基础服务;将服务类型设置为<code class="language-plaintext highlighter-rouge">LoadBalance</code>,<code class="language-plaintext highlighter-rouge">NodePort</code>类型的一种扩展——这使得服务可以通过一个专用的负载均衡器来访问,这是由<code class="language-plaintext highlighter-rouge">kubernetes</code>中正在运行的云基础设置提供的;创建一个<code class="language-plaintext highlighter-rouge">Ingress</code>服务,这是一个完全不同的机制,通过一个<code class="language-plaintext highlighter-rouge">ip</code>地址公开多个服务。</p>
</blockquote>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kubia-nodeport</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">NodePort</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
<span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
<span class="na">nodePort</span><span class="pi">:</span> <span class="m">30123</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">kubia</span>
</code></pre></div></div>
<p>在配置文件<code class="language-plaintext highlighter-rouge">kubia-svc-nodeport.yaml</code>中,<code class="language-plaintext highlighter-rouge">spec</code>部分的<code class="language-plaintext highlighter-rouge">type</code>属性值为<code class="language-plaintext highlighter-rouge">NodePort</code>类型。其中<code class="language-plaintext highlighter-rouge">targetPort</code>表示背后<code class="language-plaintext highlighter-rouge">pod</code>的目标端口号、通过<code class="language-plaintext highlighter-rouge">nodePort</code>的集群的<code class="language-plaintext highlighter-rouge">30123</code>端口可以访问该服务。通过<code class="language-plaintext highlighter-rouge">kubectl get svc kubia-nodeport</code>可以看到<code class="language-plaintext highlighter-rouge">ENTERNAL-IP</code>列数据为<code class="language-plaintext highlighter-rouge"><nodes></code>,表示服务可通过任何集群节点的<code class="language-plaintext highlighter-rouge">ip</code>地址访问。其中<code class="language-plaintext highlighter-rouge">PORT(S)</code>列显示集群<code class="language-plaintext highlighter-rouge">IP(80)</code>的内部端口和节点端口<code class="language-plaintext highlighter-rouge">(30123)</code>。可以使用<code class="language-plaintext highlighter-rouge">curl</code>命令通过<code class="language-plaintext highlighter-rouge">10.109.37.229</code>地址进行请求<code class="language-plaintext highlighter-rouge">pod</code>。在使用<code class="language-plaintext highlighter-rouge">minikube</code>时,可以运行<code class="language-plaintext highlighter-rouge">minikube service <service-name></code>命令,就可以通过浏览器轻松访问<code class="language-plaintext highlighter-rouge">NodePort</code>服务。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl create <span class="nt">-f</span> kubia-svc-nodeport.yaml
<span class="o">[</span><span class="nb">sudo</span><span class="o">]</span> password <span class="k">for </span>sam:
service/kubia-nodeport created
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get svc kubia-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT<span class="o">(</span>S<span class="o">)</span> AGE
kubia-nodeport NodePort 10.109.37.229 <none> 80:30123/TCP 17s
sam@elementoryos:~/kubernetes<span class="nv">$ </span>curl http://10.109.37.229:80
You<span class="s1">'ve hit kubia-9vds6
sam@elementoryos:~/kubernetes$ sudo minikube service kubia-nodeport
|-----------|----------------|-------------|------------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|----------------|-------------|------------------------------|
| default | kubia-nodeport | | http://192.168.170.130:30123 |
|-----------|----------------|-------------|------------------------------|
🎉 Opening kubernetes service default/kubia-nodeport in default browser...
</span></code></pre></div></div>
<p>通过负载均衡将服务暴露出来,创建<code class="language-plaintext highlighter-rouge">LoadBalance</code>服务,<code class="language-plaintext highlighter-rouge">spec.type</code>的类型为<code class="language-plaintext highlighter-rouge">LoadBalancer</code>。如果没有指定特定的节点端口,<code class="language-plaintext highlighter-rouge">kubernetes</code>将会选择一个端口。如果使用的是<code class="language-plaintext highlighter-rouge">minikube</code>,尽管负载平衡器不会被分配,仍然可以通过节点端口(位于<code class="language-plaintext highlighter-rouge">minikube vm</code>的<code class="language-plaintext highlighter-rouge">ip</code>地址)访问服务。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kubia-loadbalancer</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">LoadBalancer</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
<span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">kubia</span>
</code></pre></div></div>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get svc kubia-loadbalancer
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT<span class="o">(</span>S<span class="o">)</span> AGE
kubia-loadbalancer LoadBalancer 10.101.132.161 <pending> 80:32608/TCP 41s
</code></pre></div></div>
<p>使用<code class="language-plaintext highlighter-rouge">Ingress</code>向外暴露服务的意义:一个重要的原因是每个<code class="language-plaintext highlighter-rouge">LoadBalancer</code>服务都需要自己的负载均衡器,以及独有的公有<code class="language-plaintext highlighter-rouge">ip</code>地址,而<code class="language-plaintext highlighter-rouge">Ingress</code>只需要一个公网<code class="language-plaintext highlighter-rouge">ip</code>就能为许多服务提供访问。在介绍<code class="language-plaintext highlighter-rouge">Ingress</code>对象提供的功能之前,必须强调只有<code class="language-plaintext highlighter-rouge">Ingress</code>控制器在集群中运行,<code class="language-plaintext highlighter-rouge">Ingree</code>资源才能正常工作。由于网络限制在使用<code class="language-plaintext highlighter-rouge">minikube</code>时,并不能从外网<code class="language-plaintext highlighter-rouge">pull</code>所需的镜像。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>minikube addons <span class="nb">enable </span>ingress
✅ ingress was successfully enabled
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods <span class="nt">--all-namespaces</span>
kube-system nginx-ingress-controller-6fc5bcc8c9-7zp46 0/1 ImagePullBackOff 0 6m8s
</code></pre></div></div>
<p>使用<code class="language-plaintext highlighter-rouge">kubia-ingress.yaml</code>在<code class="language-plaintext highlighter-rouge">kubernetes</code>中创建<code class="language-plaintext highlighter-rouge">Ingress</code>资源,<code class="language-plaintext highlighter-rouge">Ingress</code>将域名<code class="language-plaintext highlighter-rouge">kubia.example.com</code>映射到你的服务,将所有的请求发送到<code class="language-plaintext highlighter-rouge">kubia-nodeport</code>服务的<code class="language-plaintext highlighter-rouge">80</code>端口。<code class="language-plaintext highlighter-rouge">Ingress</code>的工作原理:客户端通过<code class="language-plaintext highlighter-rouge">Ingress</code>控制器连接到其中一个<code class="language-plaintext highlighter-rouge">pod</code>,客户端首先对<code class="language-plaintext highlighter-rouge">kubia.example.com</code>执行<code class="language-plaintext highlighter-rouge">DNS</code>查找,<code class="language-plaintext highlighter-rouge">DNS</code>服务器返回了<code class="language-plaintext highlighter-rouge">Ingress</code>控制的<code class="language-plaintext highlighter-rouge">ip</code>。客户端然后向<code class="language-plaintext highlighter-rouge">Ingress</code>控制器发送<code class="language-plaintext highlighter-rouge">Http</code>请求,并在<code class="language-plaintext highlighter-rouge">Host</code>头中指定<code class="language-plaintext highlighter-rouge">kubia.example.com</code>。控制器从该头部确定客户端尝试访问哪个服务,通过与该服务关联的<code class="language-plaintext highlighter-rouge">Endpoint</code>对象查看<code class="language-plaintext highlighter-rouge">pod IP</code>,并将客户端的请求转发给其中一个<code class="language-plaintext highlighter-rouge">pod</code>。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">extensions/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kubia</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">rules</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">kubia.example.com</span>
<span class="na">http</span><span class="pi">:</span>
<span class="na">paths</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
<span class="na">backend</span><span class="pi">:</span>
<span class="na">serviceName</span><span class="pi">:</span> <span class="s">kubia-nodeport</span>
<span class="na">servicePort</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Ingress</code>不仅可以转发<code class="language-plaintext highlighter-rouge">http</code>流量,可以使用<code class="language-plaintext highlighter-rouge">Ingress</code>创建<code class="language-plaintext highlighter-rouge">TLS</code>进行认证,控制器将终止<code class="language-plaintext highlighter-rouge">tls</code>连接。客户端和控制器之间的通信是加密的,而控制器和后端<code class="language-plaintext highlighter-rouge">pod</code>之前的通信则不是。运行在<code class="language-plaintext highlighter-rouge">pod</code>上的应用程序是不需要<code class="language-plaintext highlighter-rouge">tls</code>,如果<code class="language-plaintext highlighter-rouge">pod</code>运行<code class="language-plaintext highlighter-rouge">web</code>服务器,则它只能接收<code class="language-plaintext highlighter-rouge">http</code>通信。要使控制器能够这样做,需要将证书和私钥附加到<code class="language-plaintext highlighter-rouge">Ingress</code>,这两个必须资源存储在称为<code class="language-plaintext highlighter-rouge">secret</code>的<code class="language-plaintext highlighter-rouge">kubernetes</code>资源中,然后在<code class="language-plaintext highlighter-rouge">Ingress manifest</code>中引用它。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get ingresses
Name Hosts Address Ports Age
kubia kubia.example.com 192.168.99.100 80 29m
sam@elementoryos:~/kubernetes<span class="nv">$ </span>curl http://kubia.example.com
You<span class="s1">'ve hit kubia-9vds6
</span></code></pre></div></div>
<h4 id="四kubernetes卷挂载用configmap和secret配置应用">四、<code class="language-plaintext highlighter-rouge">kubernetes</code>卷挂载、用<code class="language-plaintext highlighter-rouge">ConfigMap</code>和<code class="language-plaintext highlighter-rouge">Secret</code>配置应用</h4>
<p><code class="language-plaintext highlighter-rouge">kubernetes</code>的卷是<code class="language-plaintext highlighter-rouge">pod</code>的一个组成部分,因此像容器一样在<code class="language-plaintext highlighter-rouge">pod</code>的规范中做定义了。它们不是独立的<code class="language-plaintext highlighter-rouge">kubernetes</code>对象,也不能单独创建或删除。<code class="language-plaintext highlighter-rouge">pod</code>中的所有容器都可以使用卷,但必须先将它挂载在每个需要访问它的容器中。在每个容器中,都可以在其文件系统的任何位置挂载卷。</p>
<p>最简单的卷类型是<code class="language-plaintext highlighter-rouge">emptyDir</code>卷,一个<code class="language-plaintext highlighter-rouge">emptyDir</code>卷对于在同一个<code class="language-plaintext highlighter-rouge">pod</code>中运行的容器至今共享文件特别有用,其可以被单个容器用于将数据临时写入磁盘。在<code class="language-plaintext highlighter-rouge">fortune-pod.yaml</code>中<code class="language-plaintext highlighter-rouge">pod</code>包含两个容器和一个挂载在两个容器中公用的卷,但在不同的路径上。<code class="language-plaintext highlighter-rouge">html-generator</code>启动时,它每<code class="language-plaintext highlighter-rouge">10</code>秒启动一次<code class="language-plaintext highlighter-rouge">fortune</code>命令输出到<code class="language-plaintext highlighter-rouge">/var/htdocs/index.html</code>文件。当<code class="language-plaintext highlighter-rouge">web-server</code>容器启动,它就开始为<code class="language-plaintext highlighter-rouge">/usr/share/nginx/html</code>目录中的任意<code class="language-plaintext highlighter-rouge">html</code>文件提供服务,最终效果是,一个客户端向<code class="language-plaintext highlighter-rouge">pod</code>上<code class="language-plaintext highlighter-rouge">80</code>端口发送一个<code class="language-plaintext highlighter-rouge">http</code>请求,将接收当前的<code class="language-plaintext highlighter-rouge">fortune</code>消息作为响应。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fortune</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">image</span><span class="pi">:</span> <span class="s">luksa/fortune</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">html-generator</span>
<span class="na">volumeMounts</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">html</span>
<span class="na">mountPath</span><span class="pi">:</span> <span class="s">/var/htdocs</span>
<span class="pi">-</span> <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:alpine</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">web-server</span>
<span class="na">volumeMounts</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">html</span>
<span class="na">mountPath</span><span class="pi">:</span> <span class="s">/usr/share/nginx/html</span>
<span class="na">readOnly</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">80</span>
<span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">html</span>
<span class="na">emptyDir</span><span class="pi">:</span> <span class="pi">{}</span>
</code></pre></div></div>
<p>为了查看<code class="language-plaintext highlighter-rouge">fortune</code>消息,需要启用对<code class="language-plaintext highlighter-rouge">pod</code>的访问,可以尝试将端口从本地机器转发到<code class="language-plaintext highlighter-rouge">pod</code>实现。若等待几秒发送另一个请求,则应该会接收另一条消息。作为卷来使用<code class="language-plaintext highlighter-rouge">emptyDit</code>,是在承载<code class="language-plaintext highlighter-rouge">pod</code>的工作节点的实际磁盘上创建的。可以将<code class="language-plaintext highlighter-rouge">emptyDir</code>的<code class="language-plaintext highlighter-rouge">medium</code>设置为<code class="language-plaintext highlighter-rouge">Memory</code>将临时数据写入到内存中。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes/fortune<span class="nv">$ </span><span class="nb">sudo </span>kubectl port-forward fortune 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from <span class="o">[</span>::1]:8080 -> 80
Handling connection <span class="k">for </span>8080
sam@elementoryos:~/kubernetes<span class="nv">$ </span>curl http://localhost:8080
Your talents will be recognized and suitably rewarded.
sam@elementoryos:~/kubernetes<span class="nv">$ </span>curl http://localhost:8080
Your business will go through a period of considerable expansion.
</code></pre></div></div>
<p>使用<code class="language-plaintext highlighter-rouge">Git</code>仓库作为存储卷:<code class="language-plaintext highlighter-rouge">gitRepo</code>卷基本上也是一个<code class="language-plaintext highlighter-rouge">emptyDir</code>卷,它通过克隆<code class="language-plaintext highlighter-rouge">Git</code>仓库并在<code class="language-plaintext highlighter-rouge">pod</code>启动时(但在创建容器之前)检出特定版本来填充数据。在创建<code class="language-plaintext highlighter-rouge">pod</code>之前,需要有一个包含<code class="language-plaintext highlighter-rouge">html</code>文件并实际可用的<code class="language-plaintext highlighter-rouge">Git</code>仓库。创建<code class="language-plaintext highlighter-rouge">pod</code>时,首先将卷初始化为一个空目录,然后将制定的<code class="language-plaintext highlighter-rouge">Git</code>仓库克隆到其中。<code class="language-plaintext highlighter-rouge">kubernetes</code>会将分支切换到<code class="language-plaintext highlighter-rouge">master</code>上。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">html</span>
<span class="na">gitRepo</span><span class="pi">:</span>
<span class="na">repository</span><span class="pi">:</span> <span class="s">https://github.com/luksa/kubia-website-example.git</span>
<span class="na">revision</span><span class="pi">:</span> <span class="s">master</span>
<span class="na">directory</span><span class="pi">:</span> <span class="s">.</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">kubernetes</code>中某些系统级别的<code class="language-plaintext highlighter-rouge">pod</code>会使用<code class="language-plaintext highlighter-rouge">hostPath</code>访问节点文件系统上的文件,<code class="language-plaintext highlighter-rouge">hostPath</code>卷指向节点系统上的特定文件或目录。在同一个结点上运行并在其<code class="language-plaintext highlighter-rouge">hostPath</code>卷中使用相同路径的<code class="language-plaintext highlighter-rouge">pod</code>可以看到相同的文件。<code class="language-plaintext highlighter-rouge">hostPath</code>卷持久性存储,<code class="language-plaintext highlighter-rouge">gitRepo</code>和<code class="language-plaintext highlighter-rouge">emptyDir</code>卷的内容都会在<code class="language-plaintext highlighter-rouge">pod</code>被删除时被删除,而<code class="language-plaintext highlighter-rouge">hostPath</code>卷的内容则不会被删除。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods <span class="nt">--namespace</span> kube-system
<span class="o">[</span><span class="nb">sudo</span><span class="o">]</span> password <span class="k">for </span>sam:
NAME READY STATUS RESTARTS AGE
coredns-755587fdc8-nz7s8 0/1 CrashLoopBackOff 402 4d20h
etcd-minikube 1/1 Running 1 4d20h
kube-controller-manager-minikube 1/1 Running 20 4d20h
kube-proxy-gczr4 1/1 Running 1 4d20h
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl describe pod kube-proxy-gczr4 <span class="nt">--namespace</span> kube-system
Volumes:
kube-proxy:
Type: ConfigMap <span class="o">(</span>a volume populated by a ConfigMap<span class="o">)</span>
Name: kube-proxy
Optional: <span class="nb">false
</span>xtables-lock:
Type: HostPath <span class="o">(</span>bare host directory volume<span class="o">)</span>
Path: /run/xtables.lock
HostPathType: FileOrCreate
lib-modules:
Type: HostPath <span class="o">(</span>bare host directory volume<span class="o">)</span>
Path: /lib/modules
HostPathType:
kube-proxy-token-qdktp:
Type: Secret <span class="o">(</span>a volume populated by a Secret<span class="o">)</span>
SecretName: kube-proxy-token-qdktp
Optional: <span class="nb">false
</span>QoS Class: BestEffort
</code></pre></div></div>
<p>配置容器化应用程序,在<code class="language-plaintext highlighter-rouge">kubernetes</code>中使用<code class="language-plaintext highlighter-rouge">ConfigMap</code>配置<code class="language-plaintext highlighter-rouge">pod</code>应用:</p>
<blockquote>
<p>无论是否在使用<code class="language-plaintext highlighter-rouge">ConfigMap</code>存储配置数据,如下三种方式都可用于配置你的应用程序:向容器中传递命令行参数、为每个容器设置自定义环境变量、通过特殊类型的卷将配置文件挂载到容器中。</p>
</blockquote>
<p>在<code class="language-plaintext highlighter-rouge">docker</code>中定义命令与参数:<code class="language-plaintext highlighter-rouge">ENTRYPOINT</code>和<code class="language-plaintext highlighter-rouge">CMD</code>,在<code class="language-plaintext highlighter-rouge">Dockerfile</code>中的两种指令分别定义命令与参数这两部分,<code class="language-plaintext highlighter-rouge">ENTRYPOINT</code>定义容器启动时被调用的可执行程序、<code class="language-plaintext highlighter-rouge">CMD</code>指定传递给<code class="language-plaintext highlighter-rouge">ENTRYPOINT</code>的参数。在<code class="language-plaintext highlighter-rouge">fortune</code>镜像中添加<code class="language-plaintext highlighter-rouge">VARIABLE</code>变量并用第一个命令行参数对其进行初始化<code class="language-plaintext highlighter-rouge">INTERVAL=$1</code>,在<code class="language-plaintext highlighter-rouge">Dockerfile</code>中添加<code class="language-plaintext highlighter-rouge">CMD ["10"]</code>将命令行参数进行传递。</p>
<p><code class="language-plaintext highlighter-rouge">kubernetes</code>允许将配置选项分离到单独的资源对象<code class="language-plaintext highlighter-rouge">ConfigMap</code>中,本质上就是一个键/值对映射,值可以是短字面量,也可以是完整的配置文件。映射的内容通过环境变量或者卷文件的形式传递给容器,而并非直接传递给容器。命令行参数的定义中可以通过<code class="language-plaintext highlighter-rouge">${ENV_VAR}</code>语法引用环境变量,因而可以达到将<code class="language-plaintext highlighter-rouge">ConfigMap</code>的条目当作命令行参数传递给进程。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl create configmap fortune-config <span class="nt">--from-literal</span><span class="o">=</span>sleep-interval<span class="o">=</span>25
<span class="o">[</span><span class="nb">sudo</span><span class="o">]</span> password <span class="k">for </span>sam:
configmap/fortune-config created
sam@elementoryos:~/kubernetes<span class="nv">$ </span><span class="nb">sudo </span>kubectl get configmap fortune-config <span class="nt">-o</span> yaml
apiVersion: v1
data:
sleep-interval: <span class="s2">"25"</span>
kind: ConfigMap
metadata:
creationTimestamp: <span class="s2">"2019-11-24T09:51:36Z"</span>
name: fortune-config
namespace: default
resourceVersion: <span class="s2">"151450"</span>
selfLink: /api/v1/namespaces/default/configmaps/fortune-config
uid: 918d8a0a-f4a1-4b75-8f5b-e1f018a33dec
</code></pre></div></div>
<p>可以使用<code class="language-plaintext highlighter-rouge">kubectl create configmap</code> 创建<code class="language-plaintext highlighter-rouge">ConfigMap</code>,此命令支持从磁盘上读取文件,并将文件内容单独存储为<code class="language-plaintext highlighter-rouge">ConfigMap</code>中的条目。给容器传递<code class="language-plaintext highlighter-rouge">ConfigMap</code>条目作为环境变量,如<code class="language-plaintext highlighter-rouge">fortune-pod-env-configmap.yaml</code>。设置环境变量<code class="language-plaintext highlighter-rouge">INTERVAL </code>,用<code class="language-plaintext highlighter-rouge">ConfigMap</code>初始化不设置固定值,环境变量中的<code class="language-plaintext highlighter-rouge">key</code>设置为<code class="language-plaintext highlighter-rouge">sleep-interval</code>。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fortune-env-from-configmap</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">image</span><span class="pi">:</span> <span class="s">luksa/fortune:env</span>
<span class="na">env</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">INTERVAL</span>
<span class="na">valueFrom</span><span class="pi">:</span>
<span class="na">configMapKeyRef</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fortune-config</span>
<span class="na">key</span><span class="pi">:</span> <span class="s">sleep-interval</span>
</code></pre></div></div>
<p>一次性传递<code class="language-plaintext highlighter-rouge">ConfigMap</code>的所有条目作为环境变量,为每个条目单独设置环境变量的过程是单调乏味且容易出错的。在<code class="language-plaintext highlighter-rouge">kubernetes</code>的<code class="language-plaintext highlighter-rouge">1.6</code>版本提供了暴露<code class="language-plaintext highlighter-rouge">ConfigMap</code>的所有条目作为环境变量的手段。若需要将参数传递到<code class="language-plaintext highlighter-rouge">docker</code>容器内,可以通过<code class="language-plaintext highlighter-rouge">yaml</code>配置文件中设置<code class="language-plaintext highlighter-rouge">args: ["${INTERVAL}"]</code>。</p>
<p>使用<code class="language-plaintext highlighter-rouge">secret</code>给容器传递敏感数据:<code class="language-plaintext highlighter-rouge">kubernetes</code>提供了一种称为<code class="language-plaintext highlighter-rouge">secret</code>的单独资源对象。<code class="language-plaintext highlighter-rouge">secret</code>结构与<code class="language-plaintext highlighter-rouge">configMap</code>类似,均是键/值对的映射。<code class="language-plaintext highlighter-rouge">secret</code>的使用方法也与<code class="language-plaintext highlighter-rouge">configMap</code>相同,可以将<code class="language-plaintext highlighter-rouge">secret</code>条目作为环境变量传递给容器、将<code class="language-plaintext highlighter-rouge">secret</code>条目暴露给卷中的文件。</p>
<p>对于任意一个<code class="language-plaintext highlighter-rouge">pod</code>使用命令<code class="language-plaintext highlighter-rouge">kubectl describe pod</code>运行时,每个<code class="language-plaintext highlighter-rouge">pod</code>都会自动挂载上一个<code class="language-plaintext highlighter-rouge">secret</code>卷,这个卷引用的是前面<code class="language-plaintext highlighter-rouge">kubectl describe</code>输出中的一个叫做<code class="language-plaintext highlighter-rouge">default-token-bvhjx</code>的<code class="language-plaintext highlighter-rouge">secret</code>。由于<code class="language-plaintext highlighter-rouge">secret</code>也是资源对象,因此可以通过<code class="language-plaintext highlighter-rouge">kubectl get secrets</code>命令从<code class="language-plaintext highlighter-rouge">secret</code>列表中找到这个<code class="language-plaintext highlighter-rouge">default-token secret</code>。在<code class="language-plaintext highlighter-rouge">kubectl describe secrets</code>中包含三个条目——<code class="language-plaintext highlighter-rouge">ca.crt</code>、<code class="language-plaintext highlighter-rouge">namespace</code>与<code class="language-plaintext highlighter-rouge">token</code>,包含了从<code class="language-plaintext highlighter-rouge">pod</code>内部安全访问<code class="language-plaintext highlighter-rouge">kubernetes api</code>服务器所需的全部信息。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes/kubernetes-service<span class="nv">$ </span><span class="nb">sudo </span>kubectl get pods
NAME READY STATUS RESTARTS AGE
swagger-editor-z2fr6 1/1 Running 0 21s
sam@elementoryos:~/kubernetes/kubernetes-service<span class="nv">$ </span><span class="nb">sudo </span>kubectl describe pod
Volumes:
default-token-bvhjx:
Type: Secret <span class="o">(</span>a volume populated by a Secret<span class="o">)</span>
SecretName: default-token-bvhjx
Optional: <span class="nb">false
</span>sam@elementoryos:~/kubernetes/kubernetes-service<span class="nv">$ </span><span class="nb">sudo </span>kubectl get secrets
NAME TYPE DATA AGE
default-token-bvhjx kubernetes.io/service-account-token 3 5m59s
sam@elementoryos:~/kubernetes/kubernetes-service<span class="nv">$ </span><span class="nb">sudo </span>kubectl describe secrets
Name: default-token-bvhjx
Namespace: default
Labels: <none>
Annotations: kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: 6382d69c-21e6-4cdc-8193-417233ab5767
Type: kubernetes.io/service-account-token
Data
<span class="o">====</span>
ca.crt: 1066 bytes
namespace: 7 bytes
token: eyJhbGciOiJSUzI1NiIsImtpZCI6Ij
sam@elementoryos:~/kubernetes/kubernetes-service<span class="nv">$ </span><span class="nb">sudo </span>kubectl <span class="nb">exec </span>swagger-editor-z2fr6 <span class="nb">ls</span> /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt
namespace
token
</code></pre></div></div>
<p>使用<code class="language-plaintext highlighter-rouge">Downward API</code>访问<code class="language-plaintext highlighter-rouge">pod</code>的元数据以及其他资源、与<code class="language-plaintext highlighter-rouge">Kubernetes API</code>服务器交互:</p>
<blockquote>
<p>通过环境变量或者<code class="language-plaintext highlighter-rouge">configMap</code>和<code class="language-plaintext highlighter-rouge">secret</code>卷向应用传递配置数据,这对于<code class="language-plaintext highlighter-rouge">pod</code>调度、运行前预设的数据是可行的。但是那些不能预先知道的数据,如<code class="language-plaintext highlighter-rouge">pod</code>的<code class="language-plaintext highlighter-rouge">ip</code>、主机名或者<code class="language-plaintext highlighter-rouge">pod</code>自身的名称,对于此类问题,可以通过使用<code class="language-plaintext highlighter-rouge">Kubernetes download API</code>解决,这种方式主要是将在<code class="language-plaintext highlighter-rouge">pod</code>的定义和状态中取的的数据作为环境变量和文件的值。</p>
</blockquote>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">downward</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">main</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">busybox</span>
<span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">sleep"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">9999999"</span><span class="pi">]</span>
<span class="na">resources</span><span class="pi">:</span>
<span class="na">requests</span><span class="pi">:</span>
<span class="na">cpu</span><span class="pi">:</span> <span class="s">15m</span>
<span class="na">memory</span><span class="pi">:</span> <span class="s">100Ki</span>
<span class="na">limits</span><span class="pi">:</span>
<span class="na">cpu</span><span class="pi">:</span> <span class="s">100m</span>
<span class="na">memory</span><span class="pi">:</span> <span class="s">4Mi</span>
<span class="na">env</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">POD_NAME</span>
<span class="na">valueFrom</span><span class="pi">:</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">metadata.name</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">POD_NAMESPACE</span>
<span class="na">valueFrom</span><span class="pi">:</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">metadata.namespace</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">POD_IP</span>
<span class="na">valueFrom</span><span class="pi">:</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">status.podIP</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">NODE_NAME</span>
<span class="na">valueFrom</span><span class="pi">:</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">spec.nodeName</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SERVICE_ACCOUNT</span>
<span class="na">valueFrom</span><span class="pi">:</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">spec.serviceAccountName</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">downward-api-env.yaml</code>中,引用<code class="language-plaintext highlighter-rouge">pod manifest</code>中的元数据名称字段而不是设定一个具体的值。通过<code class="language-plaintext highlighter-rouge">valueFrom</code>中的<code class="language-plaintext highlighter-rouge">fieldPath</code>属性获取<code class="language-plaintext highlighter-rouge">spec.nodeName</code>元数据。在<code class="language-plaintext highlighter-rouge">yaml</code>文件中有引用<code class="language-plaintext highlighter-rouge">metadata.name</code>、<code class="language-plaintext highlighter-rouge">metadata.namespace</code>、<code class="language-plaintext highlighter-rouge">status.podIP</code>、<code class="language-plaintext highlighter-rouge">status.nodeName</code>字段值。可以使用<code class="language-plaintext highlighter-rouge">kubectl exec downward env</code>查看<code class="language-plaintext highlighter-rouge">pod</code>中的环境变量:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes/downward-api<span class="nv">$ </span><span class="nb">sudo </span>kubectl create <span class="nt">-f</span> downward-api-env.yaml
pod/downward created
sam@elementoryos:~/kubernetes/downward-api<span class="nv">$ </span><span class="nb">sudo </span>kubectl <span class="nb">exec </span>downward <span class="nb">env
</span><span class="nv">PATH</span><span class="o">=</span>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
<span class="nv">HOSTNAME</span><span class="o">=</span>downward
<span class="nv">POD_IP</span><span class="o">=</span>172.17.0.7
<span class="nv">NODE_NAME</span><span class="o">=</span>minikube
<span class="nv">SERVICE_ACCOUNT</span><span class="o">=</span>default
<span class="nv">CONTAINER_CPU_REQUEST_MILLICORES</span><span class="o">=</span>15
<span class="nv">CONTAINER_MEMORY_LIMIT_KIBIBYTES</span><span class="o">=</span>4096
<span class="nv">POD_NAME</span><span class="o">=</span>downward
<span class="nv">POD_NAMESPACE</span><span class="o">=</span>default
<span class="nv">KUBIA_SERVICE_PORT</span><span class="o">=</span>80
<span class="nv">KUBIA_PORT</span><span class="o">=</span>tcp://10.110.207.33:80
<span class="nv">KUBIA_PORT_443_TCP_ADDR</span><span class="o">=</span>10.110.207.33
</code></pre></div></div>
<p>如果更倾向于使用文件的方式而不是环境变量的方式暴露元数据,可以定义一个<code class="language-plaintext highlighter-rouge">downward API</code>卷并挂载到容器中,由于不能通过环境变量暴露,所以必须使用<code class="language-plaintext highlighter-rouge">downward API</code>卷来暴露<code class="language-plaintext highlighter-rouge">pod</code>标签或注解。与环境变量一样,需要显示地定义元器据字段来暴露份进程,我们将示例从使用环境变量修改为使用存储卷。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">...</span>
<span class="na">volumeMounts</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">downward</span>
<span class="na">mountPath</span><span class="pi">:</span> <span class="s">/etc/downward</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">downward</span>
<span class="na">downwardAPI</span><span class="pi">:</span>
<span class="na">items</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">podName"</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">metadata.name</span>
<span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">podNamespace"</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">metadata.namespace</span>
<span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">labels"</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">metadata.labels</span>
<span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">annotations"</span>
<span class="na">fieldRef</span><span class="pi">:</span>
<span class="na">fieldPath</span><span class="pi">:</span> <span class="s">metadata.annotations</span>
<span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">containerCpuRequestMilliCores"</span>
<span class="na">resourceFieldRef</span><span class="pi">:</span>
<span class="na">containerName</span><span class="pi">:</span> <span class="s">main</span>
<span class="na">resource</span><span class="pi">:</span> <span class="s">requests.cpu</span>
<span class="na">divisor</span><span class="pi">:</span> <span class="s">1m</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">downward-api-volume.yaml</code>文件中,现在并没有通过环境变量来传递元数据,而是定义了一个叫做<code class="language-plaintext highlighter-rouge">downward</code>的卷,并且通过<code class="language-plaintext highlighter-rouge">/etc/downward</code>目录挂载到我们的容器中。卷所包含的文件会通过卷定义中的<code class="language-plaintext highlighter-rouge">downwardAPI.items</code>属性来定义。若要在卷的定义中引用容器级的元数据,则需指定<code class="language-plaintext highlighter-rouge">containerName</code>属性的值为容器名称。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/kubernetes/downward-api<span class="nv">$ </span><span class="nb">sudo </span>kubectl <span class="nb">exec </span>downward <span class="nb">ls</span> /etc/downward
annotations
containerCpuRequestMilliCores
containerMemoryLimitBytes
labels
podName
podNamespace
sam@elementoryos:~/kubernetes/downward-api<span class="nv">$ </span><span class="nb">sudo </span>kubectl <span class="nb">exec </span>downward <span class="nb">cat</span> /etc/downward/labels
<span class="nv">foo</span><span class="o">=</span><span class="s2">"bar"</span>
sam@elementoryos:~/kubernetes/downward-api<span class="nv">$ </span><span class="nb">sudo </span>kubectl <span class="nb">exec </span>downward <span class="nb">cat</span> /etc/downward/annotations
<span class="nv">key1</span><span class="o">=</span><span class="s2">"value1"</span>
<span class="nv">key2</span><span class="o">=</span><span class="s2">"multi</span><span class="se">\n</span><span class="s2">line</span><span class="se">\n</span><span class="s2">value</span><span class="se">\n</span><span class="s2">"</span>
kubernetes.io/config.seen<span class="o">=</span><span class="s2">"2019-12-01T15:08:21.544699469+08:00"</span>
kubernetes.io/config.source<span class="o">=</span><span class="s2">"api"</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Downward API</code>提供了一种简单的方式,将<code class="language-plaintext highlighter-rouge">pod</code>和容器的元数据传递给在它们内部运行的进程。通过<code class="language-plaintext highlighter-rouge">kubectl cluster-info</code>命令得到服务器的<code class="language-plaintext highlighter-rouge">Url</code>。因为服务器使用<code class="language-plaintext highlighter-rouge">https</code>协议并且需要授权,所以与服务器交互并不是一件简单的事情。可以尝试通过<code class="language-plaintext highlighter-rouge">curl</code>来访问它,使用<code class="language-plaintext highlighter-rouge">curl</code>的<code class="language-plaintext highlighter-rouge">--insecure</code>选项来跳过服务器证书检查环节。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubernetes.io/config.source<span class="o">=</span><span class="s2">"api"</span>sam@elementoryos:~/kubernetes/downward-api<span class="nv">$ </span><span class="nb">sudo </span>kubectl cluster-info
<span class="o">[</span><span class="nb">sudo</span><span class="o">]</span> password <span class="k">for </span>sam:
Kubernetes master is running at https://192.168.170.128:8443
CoreDNS is running at https://192.168.170.128:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use <span class="s1">'kubectl cluster-info dump'</span><span class="nb">.</span>
sam@elementoryos:~/kubernetes/downward-api<span class="nv">$ </span><span class="nb">sudo </span>kubectl proxy
Starting to serve on 127.0.0.1:8001
sam@elementoryos:~/kubernetes<span class="nv">$ </span>curl localhost:8001
<span class="o">{</span>
<span class="s2">"paths"</span>: <span class="o">[</span>
<span class="s2">"/api"</span>,
<span class="s2">"/api/v1"</span>,
<span class="s2">"/apis"</span>,
<span class="s2">"/apis/"</span>,
<span class="s2">"/apis/admissionregistration.k8s.io"</span>
...
<span class="o">]</span>
<span class="o">}</span>
sam@elementoryos:~/kubernetes<span class="nv">$ </span>curl http://localhost:8001/apis/batch/v1/jobs
<span class="o">{</span>
<span class="s2">"kind"</span>: <span class="s2">"JobList"</span>,
<span class="s2">"apiVersion"</span>: <span class="s2">"batch/v1"</span>,
<span class="s2">"metadata"</span>: <span class="o">{</span>
<span class="s2">"selfLink"</span>: <span class="s2">"/apis/batch/v1/jobs"</span>,
<span class="s2">"resourceVersion"</span>: <span class="s2">"23398"</span>
<span class="o">}</span>,
<span class="s2">"items"</span>: <span class="o">[]</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在响应消息展示了包括可用版本,客户推荐使用版本在内的批量<code class="language-plaintext highlighter-rouge">api</code>组信息。<code class="language-plaintext highlighter-rouge">api</code>服务器返回了在<code class="language-plaintext highlighter-rouge">batch/v1</code>目录下<code class="language-plaintext highlighter-rouge">api</code>组中资源类型以及<code class="language-plaintext highlighter-rouge">rest ednpoint</code>清单。除了资源的名称和相关类型,<code class="language-plaintext highlighter-rouge">api</code>服务器也包含了一些其他信息,比如资源是否被指定了命名空间、名称简写、资源对应可以使用的动词列表等。<code class="language-plaintext highlighter-rouge">curl http://localhost:8001/apis/batch/v1/jobs</code>路径运行一个<code class="language-plaintext highlighter-rouge">GET</code>请求,可以获取集群中所有<code class="language-plaintext highlighter-rouge">Job</code>清单。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">curl-with-ambassador</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">main</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">tutum/curl</span>
<span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">sleep"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">9999999"</span><span class="pi">]</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ambassador</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">luksa/kubectl-proxy:1.6.2</span>
</code></pre></div></div>
<p>可以通过<code class="language-plaintext highlighter-rouge">embassador</code>容器简化与<code class="language-plaintext highlighter-rouge">api</code>服务器的交互,为了通过操作理解<code class="language-plaintext highlighter-rouge">ambassador</code>容器模式。我们像之前创建<code class="language-plaintext highlighter-rouge">curl pod</code>一样创建一个新的<code class="language-plaintext highlighter-rouge">pod</code>,但这次不是仅仅在<code class="language-plaintext highlighter-rouge">pod</code>中运行单个容器,而是基于一个多用途的<code class="language-plaintext highlighter-rouge">kubectl-proxy</code>容器镜像来运行一个额外的<code class="language-plaintext highlighter-rouge">ambassador</code>容器,当<code class="language-plaintext highlighter-rouge">pod</code>启动后会同时启动<code class="language-plaintext highlighter-rouge">kubectl-proxy</code>和<code class="language-plaintext highlighter-rouge">curl</code>服务。</p>
深入理解kafka消息中间件
2019-10-12T00:00:00+00:00
https://dongma.github.io/2019/10/12/kafka-streaming-system
<p><img src="../../../../resource/2019/kafka_message_system.png" alt="kafka-message-system" /></p>
<h3 id="kafka分布式消息中间件使用">Kafka分布式消息中间件使用:</h3>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">Kafka</code>是为了解决<code class="language-plaintext highlighter-rouge">LinkedIn</code>数据管道问题应用而生的,它的设计目的是提供一个高性能的消息系统,可以处理多种数据类型,并能够实时提供纯净且结构化的用户活动数据和系统度量指标。</p>
</blockquote>
<p>数据为我们所做的每一件事都提供了动力。—— <code class="language-plaintext highlighter-rouge">Jeff Weiner, LinkedIn CEO</code></p>
<h4 id="一基础环境搭建">一、基础环境搭建:</h4>
<p><code class="language-plaintext highlighter-rouge">Kafka</code>依赖于<code class="language-plaintext highlighter-rouge">Zookeeper</code>的分布式节点选举功能,安装<code class="language-plaintext highlighter-rouge">Kafka</code>需安装<code class="language-plaintext highlighter-rouge">Jdk</code>、<code class="language-plaintext highlighter-rouge">Zookeeper</code>、<code class="language-plaintext highlighter-rouge">Scala</code>组件。</p>
<p>从<code class="language-plaintext highlighter-rouge">Apache</code>官网中心下载<code class="language-plaintext highlighter-rouge">Zookeeper</code>组件,然后安装<code class="language-plaintext highlighter-rouge">Zookeeper</code>环境:
<!-- more --></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 创建zookeeper的数据目录data</span>
<span class="o">></span> mdkir /usr/local/zookeeper/data
<span class="c"># 修改zookeeper配置文件zoo.cfg中的参数信息(指定数据目录、zookeeper暴露端口号)</span>
<span class="nv">tickTime</span><span class="o">=</span>2000
<span class="nv">dataDir</span><span class="o">=</span>/usr/local/zookeeper/data
<span class="nv">clientPort</span><span class="o">=</span>2181
<span class="c"># 启动zookeeper服务,其会加载zoo.cfg作为其配置文件</span>
<span class="o">></span> /usr/local/zookeeper/bin/zkServer.sh start
</code></pre></div></div>
<p>在安装好<code class="language-plaintext highlighter-rouge">Java</code>和<code class="language-plaintext highlighter-rouge">Zookper</code>之后就可以进行安装<code class="language-plaintext highlighter-rouge">Kafka</code>消息中间件,可以从<code class="language-plaintext highlighter-rouge">Apache Kafka</code>官网下载<code class="language-plaintext highlighter-rouge">kafka</code>消息中间件,然后进行配置安装。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 创建log目录用于临时存放kafka中间件日志信息</span>
<span class="o">></span> <span class="nb">mkdir</span> /tmp/kafka-logs
<span class="c"># kafka broker启动时需要加载server.properties配置文件,指定kafka连接zookeeper地址</span>
zookeeper.connect<span class="o">=</span>localhost:2181
<span class="c"># 启动kafka-server-start服务</span>
<span class="o">></span> /usr/local/kakfa/bin/kafka-server-start.sh <span class="nt">-daemon</span> /usr/local/kafka/config/server.properties
</code></pre></div></div>
<p>搭建好基础环境后对<code class="language-plaintext highlighter-rouge">kafka</code>消息中间件进行测试,创建新的<code class="language-plaintext highlighter-rouge">topic</code>并使用<code class="language-plaintext highlighter-rouge">kafka-console-producer</code>发送消息。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 使用kafka工具创建topic, 在参数中指定zookeeper的地址、replication-factor复制比例、及分区大小</span>
sam@elementoryos:~/kafka/kafka-install<span class="nv">$ </span>./bin/kafka-topics.sh <span class="nt">--create</span> <span class="nt">--bootstrap-server</span> localhost:9092
<span class="se">\ </span><span class="nt">--replication-factor</span> 1 <span class="nt">--partitions</span> 1 <span class="nt">--topic</span> stream
<span class="c"># 查看当前broker中所有的topic列表</span>
sam@elementoryos:~/kafka/kafka-install<span class="nv">$ </span>./bin/kafka-topics.sh <span class="nt">--list</span> <span class="nt">--bootstrap-server</span> localhost:9092
__consumer_offsets
_schemas
avro-stream
stream
<span class="c"># 使用生产者客户端生产消息</span>
sam@elementoryos:~/kafka/kafka-install<span class="nv">$ </span>bin/kafka-console-producer.sh
<span class="se">\ </span><span class="nt">--broker-list</span> localhost:9092 <span class="nt">--topic</span> stream
<span class="o">></span>this<span class="s1">'s the first message
>this'</span>s another message from kafka
<span class="c"># 使用消费者客户端消费,目前暂时使用--bootstrap-server客户端无法接收到消息,--zookeeper可以正常接收</span>
sam@elementoryos:~/kafka/kafka-install<span class="nv">$ </span>bin/kafka-console-consumer.sh
<span class="se">\ </span><span class="nt">--bootstrap-server</span> localhost:9092
<span class="se">\ </span><span class="nt">--topic</span> stream <span class="nt">--from-beginning</span>
this<span class="s1">'s the first message
this'</span>s another message from kafka
</code></pre></div></div>
<h4 id="二broker和topic部分配置参数">二、<code class="language-plaintext highlighter-rouge">broker</code>和<code class="language-plaintext highlighter-rouge">topic</code>部分配置参数:</h4>
<p><code class="language-plaintext highlighter-rouge">broker</code>端常用配置信息:</p>
<p>1.<code class="language-plaintext highlighter-rouge">broker.id</code>:每个<code class="language-plaintext highlighter-rouge">broker</code>都需要一个标识符,使用<code class="language-plaintext highlighter-rouge">broker.id</code>来表示,它的默认值为<code class="language-plaintext highlighter-rouge">0</code>。其可以被设置成任何其它任意整数。这个值在整个<code class="language-plaintext highlighter-rouge">kafka</code>集群中必须是唯一的。</p>
<p>2.<code class="language-plaintext highlighter-rouge">port</code>以及<code class="language-plaintext highlighter-rouge">zookeeper.connect</code>配置:<code class="language-plaintext highlighter-rouge">kafka</code>默认是监听<code class="language-plaintext highlighter-rouge">9092</code>端口,修改<code class="language-plaintext highlighter-rouge">port</code>配置参数可以将其设置成任意其它可用的端口。若在<code class="language-plaintext highlighter-rouge">1024</code>一下,需要使用<code class="language-plaintext highlighter-rouge">root</code>权限启动<code class="language-plaintext highlighter-rouge">kafka</code>。<code class="language-plaintext highlighter-rouge">zookeeper.connect</code>是配置连接<code class="language-plaintext highlighter-rouge">zookeeper</code>的配置信息,默认连接<code class="language-plaintext highlighter-rouge">zookeeper</code>的<code class="language-plaintext highlighter-rouge">2181</code>端口。若为<code class="language-plaintext highlighter-rouge">zookeeper</code>集群,则使用<code class="language-plaintext highlighter-rouge">,</code>对<code class="language-plaintext highlighter-rouge">zookeeper</code>进行分割。</p>
<p>3.<code class="language-plaintext highlighter-rouge">log.dirs</code>以及<code class="language-plaintext highlighter-rouge">auto.create.topics.enable</code>配置:<code class="language-plaintext highlighter-rouge">kafka</code>会将所有消息都保存磁盘上,存放这些日志片段的目录就是通过<code class="language-plaintext highlighter-rouge">log.dirs</code>指定的,它是一组用逗号分割的本地文件系统路径。若<code class="language-plaintext highlighter-rouge">auto.create.topics.enable</code>配置值为<code class="language-plaintext highlighter-rouge">true</code>,处于以下三种情况时<code class="language-plaintext highlighter-rouge">kafka</code>会自动创建主题:当一个生产者开始往主题写入消息时、当一个消费者开始从主体读取消息时、当任意一个客户端向主体发送原数据时。</p>
<p>4.<code class="language-plaintext highlighter-rouge">num.recovert.threads.per.data.dir</code>:<code class="language-plaintext highlighter-rouge">kafka</code>会使用可配置线程池来处理日志片段,默认情况下每个日志目录只使用一个线程,因为这些线程只是在服务器启动和关闭时会用到。在进行恢复时使用并行操作可能会省下数小时的时间,设置此参数需要注意,所配置的数字对应的是<code class="language-plaintext highlighter-rouge">log.dirs</code>指定的单个日志目录。</p>
<p><code class="language-plaintext highlighter-rouge">topic</code>常用配置参数:</p>
<p>1.<code class="language-plaintext highlighter-rouge">number.partions</code>:该参数指定了新创建的主题将包含多少个分区,若启用了主题自动创建功能(该功能默认是启用的),主题分区的个数就是该参数指定的值(其默认值为<code class="language-plaintext highlighter-rouge">1</code>)。可以增加主题分区的个数,但不能减少分区的个数。<code class="language-plaintext highlighter-rouge">Kafka</code>集群通过分区对主题进行横向扩展,所以当有新的<code class="language-plaintext highlighter-rouge">broker</code>加入集群时,可以通过分区个数实现集群的负载均衡。</p>
<p>2.<code class="language-plaintext highlighter-rouge">log.retention.ms</code>:<code class="language-plaintext highlighter-rouge">kafka</code>通常根据时间来决定数据可以被保留多久,默认使用<code class="language-plaintext highlighter-rouge">log.retention.hours</code>参数来配置时间,默认值为<code class="language-plaintext highlighter-rouge">168</code>小时也就是一周。除此之外,还有其他两个参数<code class="language-plaintext highlighter-rouge">log.retention.minutes</code>和<code class="language-plaintext highlighter-rouge">log.retention.ms</code>,这<code class="language-plaintext highlighter-rouge">3</code>个参数的作用是一样的,都是决定消息多久以后会被删除。</p>
<p>3.<code class="language-plaintext highlighter-rouge">log.retention.bytes</code>:另一种方式是通过保留的消息字节数来判断消息是否过期,它的值通过参数<code class="language-plaintext highlighter-rouge">log.retention.bytes</code>来指定,作用在每一个分区上。也就是说,如果有一个包含<code class="language-plaintext highlighter-rouge">8</code>个分区的主题,并且<code class="language-plaintext highlighter-rouge">log.retention.bytes</code>被设置为<code class="language-plaintext highlighter-rouge">1GB</code>,那么这个主题最多可以保留<code class="language-plaintext highlighter-rouge">8GB</code>的数据。当主题分区个数增加时,整个主题可以保留的数据也随之增加。</p>
<p>4.<code class="language-plaintext highlighter-rouge">log.segment.bytes</code>:当消息到达<code class="language-plaintext highlighter-rouge">broker</code>时,它们被追加到分区的当前日志片段上。当日志片段大小达到<code class="language-plaintext highlighter-rouge">log.segment.bytes</code>指定的上限时,当前日志片段就会被关闭,一个新的日志片段被打开,前一个日志片段等待过期(其默认过期时间为<code class="language-plaintext highlighter-rouge">10</code>天)。</p>
<p>5.<code class="language-plaintext highlighter-rouge">log.segment.ms</code>:另一个可以控制日志片段关闭时间的是<code class="language-plaintext highlighter-rouge">log.segment.ms</code>,它指定过了多长时间之后日志片段就被关闭,<code class="language-plaintext highlighter-rouge">log.segment.bytes</code>和<code class="language-plaintext highlighter-rouge">log.segment.ms</code>这两个参数之间不存在互斥问题,日志片段会在大小或时间达到上限时被关闭,就看哪个条件先得到满足。</p>
<p>6.<code class="language-plaintext highlighter-rouge">message.max.bytes</code>:<code class="language-plaintext highlighter-rouge">broker</code>通过设置<code class="language-plaintext highlighter-rouge">message.max.bytes</code>参数来限制单个消息的大小,值是<code class="language-plaintext highlighter-rouge">1MB</code>。若生产者尝试发送的消息超过这个大小,不仅消息不会被接收还会返回<code class="language-plaintext highlighter-rouge">broker</code>返回的错误消息。在消费者客户端设置的<code class="language-plaintext highlighter-rouge">fetch.message.max.bytes</code>必须与服务器设置的消息大小进行协调,如果这个值比<code class="language-plaintext highlighter-rouge">message.max.bytes</code>小,那么消费者就无法比较大的消息。</p>
<h4 id="三kafka基础术语">三、<code class="language-plaintext highlighter-rouge">Kafka</code>基础术语:</h4>
<p><code class="language-plaintext highlighter-rouge">kafka</code>的数据单元称为消息,与数据库里的一个”数据行”或者一条“记录”类似,为了提高效率消息被分批写入<code class="language-plaintext highlighter-rouge">kafka</code>,批次就是一组消息(使用单独线程处理)。</p>
<p><img src="../../../../resource/2019/kafka-producer-customer-concept.png" alt="kafka-producer-customer-concept" /></p>
<p><code class="language-plaintext highlighter-rouge">kafka</code>的消息通过<code class="language-plaintext highlighter-rouge">topic</code>(主题)进行分类,主题好比数据库中的表。<code class="language-plaintext highlighter-rouge">topic</code>可以被分为若干分区,一个分区就是一个提交日志。消息以追加的方式写入分区,然后以先入先出的顺序读取。由于一个主题一般包含几个分区,因此无法在整个主题范围内保证消息的顺序,但可以保证在单个分区的顺序。</p>
<p><code class="language-plaintext highlighter-rouge">kafka broker</code>是如何持久化数据的?总的来说,<code class="language-plaintext highlighter-rouge">kafka</code>使用消息日志(<code class="language-plaintext highlighter-rouge">log</code>)来保存数据的。总的来说,<code class="language-plaintext highlighter-rouge">kafka</code>使用消息日志(<code class="language-plaintext highlighter-rouge">log</code>)来保存数据,一个日志就是磁盘上一个只能追加(<code class="language-plaintext highlighter-rouge">append only</code>)消息的物理文件。因为只能追加写入,故避免了缓慢的随机<code class="language-plaintext highlighter-rouge">I/O</code>操作,改为性能更好的顺序<code class="language-plaintext highlighter-rouge">I/O</code>操作,这也是实现<code class="language-plaintext highlighter-rouge">kafka</code>高吞吐量特性的一个重要手段。为了避免日志写满磁盘空间,<code class="language-plaintext highlighter-rouge">kafka</code>必然要定期地删除消息以回收磁盘。其通过<code class="language-plaintext highlighter-rouge">log segment</code>机制,在<code class="language-plaintext highlighter-rouge">kafka</code>底层一个日志又近一步细分成多个日志片段,消息被追加写到当前新的日志段中。<code class="language-plaintext highlighter-rouge">kafka</code>在后台通过定时任务会定期检查老的日志段是否能够被删除,从而实现回收磁盘空间的目的。</p>
<p><img src="../../../..//resource/2019/kafka-partition-concept.png" alt="kafka-partition-concept" /></p>
<p><code class="language-plaintext highlighter-rouge">kafka</code>中分区机制指的是将每个主题划分多个分区(<code class="language-plaintext highlighter-rouge">partition</code>),每个分区是一组有序的消息日志。也就是说如果向一个双分区的主题发送一条消息,这条消息要么在分区<code class="language-plaintext highlighter-rouge">0</code>中,要么在分区<code class="language-plaintext highlighter-rouge">1</code>中。</p>
<p><code class="language-plaintext highlighter-rouge">offset</code>消费者位移:每个消费者在消费消息的过程中必然需要有个字段记录它当前消费到了分区的哪个位置上,这个字段就是消费者位移(<code class="language-plaintext highlighter-rouge">consumer offset</code>)。上面的位移表征的是分区内的消息位置,它是不变的,即一旦消息被成功写入到一个分区上,它的位移值就固定了。而消费者位移则会随着消息消费而发生变化,毕竟它是消费者消费进度的指示器。另外每个消费者都有着自己的消费者位移,因此一定要区分这两类位移的区别。</p>
<p><code class="language-plaintext highlighter-rouge">kafka</code>消费者会往一个叫做<code class="language-plaintext highlighter-rouge">_consumer_offset</code>的特殊主题发送消息,消息里包含每个分区的偏移量。在发生<code class="language-plaintext highlighter-rouge">rebalance</code>之后,为了能够继续之前的工作,消费者需要读取每一个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。当提交<code class="language-plaintext highlighter-rouge">commit</code>的偏移量小于客户端处理的最后一条消息的偏移量,当处于再均衡时会被重新处理导致重复。若提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失。</p>
<h4 id="四kafka整合confluentio-schema-registry">四、kafka整合confluent.io schema registry:</h4>
<p>使用<code class="language-plaintext highlighter-rouge">apache avro</code>实现在生产者与消费者中对消息内容进行序列化与反序列化,<code class="language-plaintext highlighter-rouge">Avro</code>是一种与编程语言无关的序列化格式。<code class="language-plaintext highlighter-rouge">Doug Cutting</code>创建了这个项目,目的是提供一种共享数据文件的方式。</p>
<p><code class="language-plaintext highlighter-rouge">Avro</code>数据通过与语言无关的<code class="language-plaintext highlighter-rouge">schema</code>来定义,<code class="language-plaintext highlighter-rouge">schema</code>通过<code class="language-plaintext highlighter-rouge">JSON</code>来描述,数据被序列化为二进制或者<code class="language-plaintext highlighter-rouge">JSON</code>文件,不过一般会使用二进制文件。<code class="language-plaintext highlighter-rouge">Avro</code>在读写文件时需要用到<code class="language-plaintext highlighter-rouge">schema</code>,<code class="language-plaintext highlighter-rouge">schema</code>一般会被内嵌在数据文件里。<code class="language-plaintext highlighter-rouge">Avro</code>有一个很有意思的特性是,当负责写消息的应用程序使用了新的<code class="language-plaintext highlighter-rouge">schema</code>,负责读消息的应用程序可以继续处理消息而无须做任何改动,这个特性使得它特别适合用在像<code class="language-plaintext highlighter-rouge">kafka</code>这样的消息系统上。</p>
<p><code class="language-plaintext highlighter-rouge">confluent </code>在其共有平台发布了<code class="language-plaintext highlighter-rouge">confluent schema registry</code>工具,作为注册表<code class="language-plaintext highlighter-rouge">schema</code>的实现。 可以从 https://www.confluent.io/download/ 进行下载,之后在服务器上启动<code class="language-plaintext highlighter-rouge">schema registry</code>服务。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos: ~/kafka_schema_registry/confluent-tools-kafka<span class="nv">$ </span>bin/schema-registry-start
<span class="se">\ </span>etc/schema-registry/schema-registry.properties
<span class="o">[</span>2019-11-12 00:13:01,160] INFO Logging initialized @1547ms to org.eclipse.jetty.util.log.Slf4jLog <span class="o">(</span>org.eclipse.jetty.util.log:193<span class="o">)</span>
</code></pre></div></div>
<p>然后将需要进行序列化实体的<code class="language-plaintext highlighter-rouge">schema</code>注册到<code class="language-plaintext highlighter-rouge">schema registry</code>中,最终其会返回一个<code class="language-plaintext highlighter-rouge">id</code>表示注册成功。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos: curl <span class="nt">-X</span> POST <span class="nt">-H</span> <span class="s2">"Content-Type: application/vnd.schemaregistry.v1+json"</span> <span class="nt">--data</span>
<span class="se">\ </span><span class="s1">'{"schema": "{\"type\": \"record\", \"name\": \"Customer\", \"fields\": [{\"name\": \"customerName\", \"type\": \"string\"}, {\"name\":\"customerId\",\"type\":\"int\"}]}"}'</span>
<span class="se">\ </span>http://192.168.170.130:8081/subjects/avro-stream-value/versions
<span class="o">{</span><span class="s2">"id"</span>:21<span class="o">}</span>
</code></pre></div></div>
<p>注册完成后,就可以分别在生产者和消费者的代码示例中使用<code class="language-plaintext highlighter-rouge">avro</code>进行序列化对象。其<code class="language-plaintext highlighter-rouge">maven</code>仓库的一些依赖包目前没有办法获取到,必须在<code class="language-plaintext highlighter-rouge">pom.xml</code>中配置其<code class="language-plaintext highlighter-rouge">repository</code>地址。同时在生产者和消费者的<code class="language-plaintext highlighter-rouge">properties</code>指定属性<code class="language-plaintext highlighter-rouge"> kafkaProperties.put("schema.registry.url", "http://192.168.170.130:8081") </code>。</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><repository></span>
<span class="nt"><id></span>confluent<span class="nt"></id></span>
<span class="nt"><url></span>http://packages.confluent.io/maven/<span class="nt"></url></span>
<span class="nt"><releases></span>
<span class="nt"><enabled></span>true<span class="nt"></enabled></span>
<span class="nt"></releases></span>
<span class="nt"><snapshots></span>
<span class="nt"><enabled></span>true<span class="nt"></enabled></span>
<span class="nt"></snapshots></span>
<span class="nt"></repository></span>
</code></pre></div></div>
<h4 id="五kafka生产者向kafka写入数据">五、kafka生产者—向kafka写入数据</h4>
<p>向<code class="language-plaintext highlighter-rouge">kafka</code>发送数据从创建<code class="language-plaintext highlighter-rouge">ProducerRecord</code>对象开始,其包含目标主题、要发送的内容,还可以指定键或分区。在发送<code class="language-plaintext highlighter-rouge">ProducerRecord</code>对象时,生产者要把键和值对象序列化成字节数组,这样其就可以在网络上传输。</p>
<p>接下来,将数据传给分区器。如果之前在<code class="language-plaintext highlighter-rouge">ProducerRecord</code>对象中指定了分区,那么分区器不会做任何事情,直接把指定的分区返回。若没有指定分区,那么分区器会根据<code class="language-plaintext highlighter-rouge">ProducerRecord</code>对象的键来选择一个分区。选好分区后,生产者就知道该往哪个主体和分区发送这条记录了。紧接着,这条记录会被添加到一个记录批次里,这个批次里的所有消息被发送到相同的主题和分区上。有一个单独的线程负责把这些记录批次发送到相应的<code class="language-plaintext highlighter-rouge">broker</code>上。</p>
<p>服务器在收到这些消息时会返回一个响应,如果消息成功写入<code class="language-plaintext highlighter-rouge">kafka</code>,就返回一个<code class="language-plaintext highlighter-rouge">RecordMetaData</code>对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误,生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败,就返回错误信息。</p>
<p><img src="../../../..//resource/2019/kafka_consume_mechanism.png" alt="kafka_consume_mechanism" /></p>
<h4 id="六kafka消费者从kafka读取数据">六、kafka消费者—从kafka读取数据</h4>
<p><code class="language-plaintext highlighter-rouge">kakfa</code>消费者从属于消费者群组,一个群组里的消费者订阅的是同一个主题,每个消费者接收主题一部分分区的消息。若消费者组中消费者的数量与主题分区的数量相等,则每一个消费者单独消费一个分区。当消费者组中消费者数量大于主题分区的数量,多余的消费者不会被分配到任何数据分区。引入消费者组的概念主要是为了提升消费者端的吞吐量。多个消费者实例同时消费,加速整个消费端的吞吐量(<code class="language-plaintext highlighter-rouge">TPS</code>)。消费者组里面的所有消费者实例不仅”瓜分”订阅主体的数据,而且更酷的是它们还能彼此协助。</p>
<p><code class="language-plaintext highlighter-rouge">Rebalance</code>概念:群组中的消费者共同读取主题的分区,一个新的消费者加入群组时,它读取的是原本由其他消费者读取的消息。当一个消费者被关闭或发生崩溃时,它就离开群组,原本由它读取的分区将由群组里的其它消费者来读取。分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡,在<code class="language-plaintext highlighter-rouge">rebalance</code>时会产生<code class="language-plaintext highlighter-rouge">stop the world</code>的问题。</p>
<p><code class="language-plaintext highlighter-rouge">kafka</code>检测方式:消费者通过向被指派为群组协调器的<code class="language-plaintext highlighter-rouge">broker</code>(不同的群组可以有不同的协调器)发送心跳来维持他们和群组的从属关系。只要消费者以正常的时间发送心跳,就被认为是活跃的,说明它还在读分区里的消息。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发一次再均衡。</p>
<p>分配分区的过程:当消费者要加入群组时,它会向群组协调器发送一个<code class="language-plaintext highlighter-rouge">JoinGroup</code>的请求。第一个加入群组的消费者将成为“群主”。群主从协调器那里获得群组的成员列表(列表中包含了所有最近发送过心跳的消费者,它们被认为是活跃的),并负责给每一个消费者分配分区。它使用了一个实现了<code class="language-plaintext highlighter-rouge">PartitionAssign</code>接口的类来决定哪些分区应该被分配给哪个消费者。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nc">Map</span><span class="o"><</span><span class="nc">TopicPartition</span><span class="o">,</span> <span class="nc">OffsetAndMetadata</span><span class="o">></span> <span class="n">currentOffsets</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o"><>();</span>
<span class="c1">// 当从kafka server中poll 200条记录,当处理了50条记录时,可以立即进行提交</span>
<span class="n">currentOffsets</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="k">new</span> <span class="nc">TopicPartition</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">topic</span><span class="o">(),</span> <span class="n">record</span><span class="o">.</span><span class="na">partition</span><span class="o">()),</span> <span class="k">new</span> <span class="nc">OffsetAndMetadata</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">offset</span><span class="o">()</span> <span class="o">+</span> <span class="mi">1</span><span class="o">,</span> <span class="s">"no metadata"</span><span class="o">));</span>
<span class="n">consumer</span><span class="o">.</span><span class="na">commitAsync</span><span class="o">(</span><span class="n">currentOffsets</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
</code></pre></div></div>
<p>提交特定的偏移量调用的是<code class="language-plaintext highlighter-rouge">commitAsync()</code>,不过调用<code class="language-plaintext highlighter-rouge">commitSync()</code>也是完全可以的。当然,在提交特定偏移量时,仍然要处理可能发生的错误。</p>
<p><code class="language-plaintext highlighter-rouge">kafka</code>的再均衡监听器:消费者在退出和进行分区再均衡之前,会做一些清理工作。需要在消费者失去对一个分区的所有权之前提交最后一个已处理记录的偏移量。如果消费者准备了一个缓冲区用于处理偶发的事件,那么在失去分区所有权之前,需要处理在缓冲区累积下来的记录。你可能还需要关闭文件句柄、数据库连接等。</p>
<p><code class="language-plaintext highlighter-rouge">ConsumerRebalanceListener</code>有两个需要实现的方法:</p>
<p>1)<code class="language-plaintext highlighter-rouge">public void onPartitionRevoked(Collection<TopicPartition> partitions)</code>方法会在再均衡开始之前和消费者停止读取消息之后被调用。如果在这里提交偏移量,下一个接管分区的消费者就知道该从哪里开始读取了。</p>
<p>2)<code class="language-plaintext highlighter-rouge">public void onPartitionsAssigned(Collection<TopicPartition> partitions)</code>方法会在重新分配分区之后和消费者开始读取消息之前被调用。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 在consumer订阅主体topic时设定回调类HandleRebalance</span>
<span class="n">consumer</span><span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="n">topics</span><span class="o">,</span> <span class="k">new</span> <span class="nc">HandleRebalance</span><span class="o">());</span>
</code></pre></div></div>
<p>从特定偏移量处开始处理记录:使用<code class="language-plaintext highlighter-rouge">poll()</code>方法从各个分区的最新偏移量处开始处理消息,有时候我们也需要从特定的偏移量处开始读取消息。<code class="language-plaintext highlighter-rouge">seekToBeginning(Collection<TopicPartition> tp)</code>和<code class="language-plaintext highlighter-rouge">seekToEnd(Collection<TopicPartition> tp)</code>这两个方法。若循环运行在主线程中,可以在<code class="language-plaintext highlighter-rouge">ShutdownHook</code>里调用该方法,需记住<code class="language-plaintext highlighter-rouge">consumer.wakeup()</code>是消费者唯一一个可以从其他线程里安全调用的方法。调用<code class="language-plaintext highlighter-rouge">consumer.wakeup()</code>可以退出<code class="language-plaintext highlighter-rouge">poll()</code>并抛出<code class="language-plaintext highlighter-rouge">WakeupException</code>异常,或者如果调用<code class="language-plaintext highlighter-rouge">consumer.wakeup()</code>时线程没有等待轮询,那么异常将在下一轮<code class="language-plaintext highlighter-rouge">poll()</code>时抛出。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Runtime</span><span class="o">.</span><span class="na">getRuntime</span><span class="o">().</span><span class="na">addShutdownHook</span><span class="o">(</span><span class="k">new</span> <span class="nc">Thread</span><span class="o">()</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
<span class="n">consumer</span><span class="o">.</span><span class="na">wakeUp</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">});</span>
</code></pre></div></div>
<h4 id="七深入理解kafka运行机制">七、深入理解kafka运行机制</h4>
<p><code class="language-plaintext highlighter-rouge">kafka</code>使用<code class="language-plaintext highlighter-rouge">zookeeper</code>来维护集群成员的信息,每个<code class="language-plaintext highlighter-rouge">broker</code>都有一个唯一标识符,这个标识符可以在配置文件中指定,也可以自动生成。在<code class="language-plaintext highlighter-rouge">broker</code>启动时,它通过创建临时节点把自己的<code class="language-plaintext highlighter-rouge">id</code>注册到<code class="language-plaintext highlighter-rouge">zookeeper</code>上。控制器<code class="language-plaintext highlighter-rouge">controller</code>机制:控制器其负责分区首领的选举,集群里第一个启动的<code class="language-plaintext highlighter-rouge">broker</code>通过在<code class="language-plaintext highlighter-rouge">zookeeper</code>里创建一个临时节点<code class="language-plaintext highlighter-rouge">/controller</code>让自己成为控制器。当其它的<code class="language-plaintext highlighter-rouge">broker</code>进行创建时,会收到一个”节点已存在”的异常,然后”意识”到控制器节点已存在,也就是说集群里已经有一个控制器了(结合<code class="language-plaintext highlighter-rouge">zookeeper</code>进行结点选举)。</p>
<p>1) <code class="language-plaintext highlighter-rouge">kafka</code>中复制是如何进行实现的?</p>
<p><code class="language-plaintext highlighter-rouge">kafka</code>使用主题来组织数据,每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在<code class="language-plaintext highlighter-rouge">broker</code>上,每个<code class="language-plaintext highlighter-rouge">broker</code>可以保存成百上千个属于不同主题和分区的副本。副本分为两种类型:首领副本,为保持一致性<code class="language-plaintext highlighter-rouge">kafka</code>中所有生产者请求和消费者请求都会经过这个副本。<code class="language-plaintext highlighter-rouge">follower</code>副本,其主要是从<code class="language-plaintext highlighter-rouge">master</code>复制消息并与<code class="language-plaintext highlighter-rouge">master</code>上内容保持一致,若<code class="language-plaintext highlighter-rouge">master</code>节点崩溃,参与节点选举并提升为新首领(<code class="language-plaintext highlighter-rouge">follower</code>副本不参与读、写)。</p>
<p>与<code class="language-plaintext highlighter-rouge">master</code>的同步实现:<code class="language-plaintext highlighter-rouge">follower</code>为了与首领同步,向首领发送获取数据的请求,<code class="language-plaintext highlighter-rouge">master</code>通过查看每个<code class="language-plaintext highlighter-rouge">follower</code>请求的最新偏移量,就可以知道每个跟随者复制的进度。如果跟随者在<code class="language-plaintext highlighter-rouge">10s</code>内没有请求任何消息,或者虽然在请求消息,但在<code class="language-plaintext highlighter-rouge">10s</code>内没有请求最新的数据,那么它就会被认为是不同步的。跟随者的正常不活跃时间或在成为不同步副本之前的时间是通过<code class="language-plaintext highlighter-rouge">replica.lag.time.max.ms</code>参数来配置的。</p>
<p>2) <code class="language-plaintext highlighter-rouge">kafka</code>是如何处理来自生产者和消费者的请求?</p>
<p>生产请求和获取请求都必须发送给分区的首领副本,客户端使用元数据请求包含了客户端感兴趣的主题列表。服务器端的响应中指明了这些主题所包含的分区、每个分区都有哪些副本、以及哪个副本是<code class="language-plaintext highlighter-rouge">master</code>节点。客户端一般会缓存这些信息,并直接往目标<code class="language-plaintext highlighter-rouge">broker</code>上发送请求和获取请求(时间间隔通过<code class="language-plaintext highlighter-rouge">metadata.max.age.ms</code>来配置)。</p>
<p>在生产者配置中存在<code class="language-plaintext highlighter-rouge">acks</code>这个配置参数——该参数指定了需要多少个<code class="language-plaintext highlighter-rouge">broker</code>确认才可以认为一个消息写入是成功的,<code class="language-plaintext highlighter-rouge">acks=all</code>需要所有<code class="language-plaintext highlighter-rouge">broker</code>收到消息才会成功;<code class="language-plaintext highlighter-rouge">acks=0</code>意味着生产者在把消息发出去之后,完全不需要等待<code class="language-plaintext highlighter-rouge">broker</code>的响应。</p>
<p>客户端发送消费请求时向<code class="language-plaintext highlighter-rouge">broker</code>主题分区里具有特定偏移量的消息,客户端还可以指定<code class="language-plaintext highlighter-rouge">broker</code>返回的数据分配足够的内存。否则,<code class="language-plaintext highlighter-rouge">broker</code>返回的大量数据有可能耗尽客户端的内存。</p>
<p>3) <code class="language-plaintext highlighter-rouge">kafka</code>的存储细节,如文件格式和索引?</p>
<p><code class="language-plaintext highlighter-rouge">kafka</code>的基本存储单元是分区,分区无法在多个<code class="language-plaintext highlighter-rouge">broker</code>间进行再细分,也无法在同一个<code class="language-plaintext highlighter-rouge">broker</code>的多个磁盘上进行再细分。在配置<code class="language-plaintext highlighter-rouge">kafka</code>时候,管理员指定了一个用于存储分区的目录清单——也就是<code class="language-plaintext highlighter-rouge">log.dirs</code>参数的值,该参数一般会包含每个挂载点的目录。</p>
<p>文件管理部分,<code class="language-plaintext highlighter-rouge">kafka</code>管理员为每个主题配置了数据保留期限,规定数据被删除之前可以保留多长时间,或者清理数据之前可以保留的数据量大小。通常分区被分成若干个片段,默认情况下,每个片段包含<code class="language-plaintext highlighter-rouge">1GB</code>或一周的数据,以较小的那个为准。在<code class="language-plaintext highlighter-rouge">broker</code>往分区写入数据时,如果达到片段上限,就关闭当前文件,并打开一个新文件。当前正在写入数据非片段叫作活跃片段,活动片段永远不会被删除。</p>
<p>消息和偏移量保存在文件里,其格式除了键、值和偏移量外,消息里还包含了消息大小、校验和、消息格式版本号、压缩算法(<code class="language-plaintext highlighter-rouge">Snappy</code>、<code class="language-plaintext highlighter-rouge">GZip</code>或<code class="language-plaintext highlighter-rouge">LZ4</code>)和时间戳。时间戳可以是生产者发送消息的时间,也可以是消息到达<code class="language-plaintext highlighter-rouge">broker</code>的时间,其是可以配置的。为了能快速从任意可用偏移量位置开始读取消息,<code class="language-plaintext highlighter-rouge">kafka</code>为每个分区维护了一个索引,索引把偏移量映射到片段文件和偏移量在文件里的位置。</p>
<p>清理工作原理:若<code class="language-plaintext highlighter-rouge">kafka</code>启动时启用了清理功能(通过配置<code class="language-plaintext highlighter-rouge">log.cleaner.enabled</code>参数),每个<code class="language-plaintext highlighter-rouge">broker</code>会启动一个清理管理器线程或多个清理线程,它们负责执行清理任务。这个线程会选择污浊率(污浊消息占分区总大小的比例)较高的分区进行清理。</p>
<p>为了清理分区,清理线程会读取分区的污浊部分,并在内存里创建一个<code class="language-plaintext highlighter-rouge">map</code>。<code class="language-plaintext highlighter-rouge">map</code>里的每个元素包含了消息键的散列值和消息的偏移量,键的散列值是<code class="language-plaintext highlighter-rouge">16B</code>,加上偏移量总共是<code class="language-plaintext highlighter-rouge">24B</code>。如果要清理一个<code class="language-plaintext highlighter-rouge">1GB</code>的日志偏亮,并假设每个消息大小为<code class="language-plaintext highlighter-rouge">1KB</code>,那么这个片段就包含一百万个消息,而我们只需要<code class="language-plaintext highlighter-rouge">24MB</code>的<code class="language-plaintext highlighter-rouge">map</code>就可以清理这个片段(若有重复的键,可以重用散列项,从而使用更少的内存)。</p>
使用Docker构建微服务镜像
2019-09-23T00:00:00+00:00
https://dongma.github.io/2019/09/23/docker-plugins
<p><img src="https://raw.githubusercontent.com/dongma/springcloud/master/document/images/1569252813951.png" alt="1569252813951" />
<!-- more --></p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">Docker</code>包括一个命令行程序、一个后台守护进程,以及一组远程服务。它解决了常见的软件问题,并简化了安装、运行、发布和删除转件。这一切能够实现是通过使用一项<code class="language-plaintext highlighter-rouge">UNIX</code>技术,称为容器。</p>
</blockquote>
<p>事实上,<code class="language-plaintext highlighter-rouge">Docker</code>项目确实与<code class="language-plaintext highlighter-rouge">Cloud Foundry</code>的容器在大部分功能和实现原理上都是一样的,可偏偏就是这剩下的一小部分不一样的功能成为了<code class="language-plaintext highlighter-rouge">Docker</code>呼风唤雨的不二法宝,这个功能就是<code class="language-plaintext highlighter-rouge">Docker</code>镜像。</p>
<p>与传统的<code class="language-plaintext highlighter-rouge">PaaS</code>项目相比,<code class="language-plaintext highlighter-rouge">Docker</code>镜像解决的恰恰就是打包这个根本性问题。所谓的<code class="language-plaintext highlighter-rouge">Docker</code>镜像,其实就是一个压缩包。但是这个压缩包中的内容比<code class="language-plaintext highlighter-rouge">PaaS</code>的应用可执行文件+启停脚本的组合就要丰富多了。实际上,大多数<code class="language-plaintext highlighter-rouge">Docker</code>镜像是直接由一个完整操作系统的所有文件和目录构成的,所以这个压缩包内容和本地开发、测试环境用的操作系统是完全一样的,这正是<code class="language-plaintext highlighter-rouge">Docker</code>镜像的精髓所在。</p>
<p>所以,<code class="language-plaintext highlighter-rouge">Docker</code>项目给<code class="language-plaintext highlighter-rouge">PaaS</code>世界带来的”降维打击”,其实是提供了一种非常便利的打包机制。这种机制直接打包了应用运行所需要的整个操作系统,从而保证了应用运行所需要的整个操作系统,从而保证了本地环境和云端环境的高度一致,避免了用户通过”试错”来匹配两种不同的运行环境之间差异的痛苦过程。</p>
<h3 id="1-容器技术基础概念">1. 容器技术基础概念</h3>
<p><code class="language-plaintext highlighter-rouge">Docker</code>容器中的运行就像是其中的一个进程,对于进程来说,它的静态表现就是程序,平常都安安静静地待在磁盘上。而一旦运行起来,它就变成了计算机里的数据和状态的总和,这就是它的动态表现。而容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个”边界”。</p>
<p>对于<code class="language-plaintext highlighter-rouge">Docker</code>等大多数<code class="language-plaintext highlighter-rouge">Linux</code>容器来说,<code class="language-plaintext highlighter-rouge">Cgroups</code>技术是用来制造约束的主要手段,而<code class="language-plaintext highlighter-rouge">Namespace</code>技术则是用来修改进程视图的主要方法。在<code class="language-plaintext highlighter-rouge">Docker</code>里容器中进程号始终是从<code class="language-plaintext highlighter-rouge">1</code>开始,容器中运行的进程已经被<code class="language-plaintext highlighter-rouge">Docker</code>隔离在了一个跟宿主机完全不同的世界当中。</p>
<p><strong>1)Namespace修改Docker进程的视图</strong>,在<code class="language-plaintext highlighter-rouge">linux</code>中创建线程的系统调用<code class="language-plaintext highlighter-rouge">clone()</code>函数,这个系统调用会为我们返回一个新的进程,并且返回它的进程号<code class="language-plaintext highlighter-rouge">pid</code>。而当我们用<code class="language-plaintext highlighter-rouge">clone()</code>函数调用和创建一个新进程时,就可以在参数中执行<code class="language-plaintext highlighter-rouge">CLONE_NEWPID</code>参数。这时,新创建的这个进程将会看到一个全新的进程空间,在这个进程空间里,它的<code class="language-plaintext highlighter-rouge">pid</code>为1。之所以所看到,是因为使用了”障眼法”,在宿主机真实的进程空间里,这个进程的<code class="language-plaintext highlighter-rouge">pid</code>还是真实的数值,比如<code class="language-plaintext highlighter-rouge">100</code>:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">pid</span> <span class="o">=</span> <span class="n">clone</span><span class="p">(</span><span class="n">main_function</span><span class="p">,</span> <span class="n">stack_size</span><span class="p">,</span> <span class="n">SIGCHLD</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
<span class="cp"># 创建新的线程指定CLONE_NEWPID,返回新的进程空间的id
</span><span class="kt">int</span> <span class="n">pid</span> <span class="o">=</span> <span class="n">clone</span><span class="p">(</span><span class="n">main_function</span><span class="p">,</span> <span class="n">stack_size</span><span class="p">,</span> <span class="n">CLONE_NEWPID</span><span class="o">|</span><span class="n">SIGCHLD</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</code></pre></div></div>
<p>当然,我们还可以多次执行上面的<code class="language-plaintext highlighter-rouge">clone()</code>调用,这样就会创建多个<code class="language-plaintext highlighter-rouge">Pid Namespace</code>,而每个<code class="language-plaintext highlighter-rouge">namespace</code>里的应用进程都会被认为自己是当前容器里的第<code class="language-plaintext highlighter-rouge">1</code>号进程,它们既看不到宿主机里真正的进程空间,也看不到其它<code class="language-plaintext highlighter-rouge">PID Namespace</code>里的具体情况。除过刚才提到的<code class="language-plaintext highlighter-rouge">PID Namespace</code>,<code class="language-plaintext highlighter-rouge">Linux</code>操作系统还提供了<code class="language-plaintext highlighter-rouge">Mount</code>、<code class="language-plaintext highlighter-rouge">UTS</code>、<code class="language-plaintext highlighter-rouge">IPC</code>、<code class="language-plaintext highlighter-rouge">Network</code>和<code class="language-plaintext highlighter-rouge">User</code>这些<code class="language-plaintext highlighter-rouge">Namespace</code>用来对各种不同的进程上下文进行“障眼法”操作。</p>
<p>“敏捷”和“高性能”是容器相较于虚拟机最大的优势,也是它能够在<code class="language-plaintext highlighter-rouge">PaaS</code>这种更细粒度的资源管理平台上大行其道的重要原因。不过,有利也有弊,基于<code class="language-plaintext highlighter-rouge">linux namespace</code>的隔离机制相比较与虚拟化技术也有很多不足之处,其中最主要的问题就是:隔离得不彻底。首先,既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机操作系统内核。其次,在<code class="language-plaintext highlighter-rouge">linux</code>内核中,有很多资源和对象是不能被<code class="language-plaintext highlighter-rouge">namespace</code>化的,最典型的例子就是:时间(若在容器中应用程序改变了系统时间,则整个宿主机的时间都会被随之修改)。</p>
<p><strong>2)在介绍完容器的”隔离”技术之后,我们再来研究一下容器的”限制”问题</strong>。虽然容器内的第<code class="language-plaintext highlighter-rouge">1</code>号进程在“障眼法”的干扰下只能看到容器里的情况,但是宿主机上它作为第<code class="language-plaintext highlighter-rouge">100</code>号进程与其他所有进程之间仍然是平等的竞争关系。虽然第<code class="language-plaintext highlighter-rouge">100</code>号进程表面上被隔离了起来,但是它所能够使用到的资源(如<code class="language-plaintext highlighter-rouge">CPU</code>、内存)却是可以随时被宿主机上的其他进程占用的。当然,这个<code class="language-plaintext highlighter-rouge">100</code>号进程自己也可能把所有资源吃光。这些情况,显然都不是一个“沙盒”应该表现出来的合理行为。</p>
<p>而<code class="language-plaintext highlighter-rouge">linux Cgroups</code>就是<code class="language-plaintext highlighter-rouge">linux</code>内核中用来为进程设置资源限制的一个重要功能,<code class="language-plaintext highlighter-rouge">linux Cgroups</code>的全称是<code class="language-plaintext highlighter-rouge">linux Control Group</code>。它的主要作用,就是限制一个进程组能够使用的资源上线,包括<code class="language-plaintext highlighter-rouge">CPU</code>、内存、磁盘、网络带宽等。此外,<code class="language-plaintext highlighter-rouge">Cgroups</code>还能够对进程进行优先级设置、审计,以及将进程挂起和修复等操作。在<code class="language-plaintext highlighter-rouge">/sys/fs/cgroup</code>下面有很多诸如<code class="language-plaintext highlighter-rouge">cpuset</code>、<code class="language-plaintext highlighter-rouge">cpu</code>、<code class="language-plaintext highlighter-rouge">memory</code>这样的子目录,也称为子系统。这些都是我这台机器当前可以被<code class="language-plaintext highlighter-rouge">Cgroups</code>进行限制的资源种类,而在子系统对应的资源种类下,就可以看到该类资源具体可以被限制的方法。如<code class="language-plaintext highlighter-rouge">cpu</code>的子系统,可以看到如下几个配置文件:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">ls</span> /sys/fs/cgroup/cpu
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.stat tasks
</code></pre></div></div>
<p>若熟悉<code class="language-plaintext highlighter-rouge">linux cpu</code>管理的话,就会在输出中注意到<code class="language-plaintext highlighter-rouge">cfs_period</code>和<code class="language-plaintext highlighter-rouge">cfs_quota</code>这样的关键字。这两个参数需要组合使用,可以用来限制进程在长度为<code class="language-plaintext highlighter-rouge">cfs_period</code>的一段时间内,只能被分配到总量为<code class="language-plaintext highlighter-rouge">cfs_quota</code>的<code class="language-plaintext highlighter-rouge">cpu</code>时间。在<code class="language-plaintext highlighter-rouge">tasks</code>文件中通常用来放置资源被限制的进程的<code class="language-plaintext highlighter-rouge">id</code>号,会对该进程进行<code class="language-plaintext highlighter-rouge">cpu</code>使用资源限制。除了<code class="language-plaintext highlighter-rouge">cpu</code>子系统外,<code class="language-plaintext highlighter-rouge">Cgroups</code>的每一项子系统都有其独有的资源限制能力,比如:<code class="language-plaintext highlighter-rouge">blkio</code>为块设置设置<code class="language-plaintext highlighter-rouge">I/O</code>限制,一般用于磁盘等设备。<code class="language-plaintext highlighter-rouge">cpuset</code>为进程分配单独的<code class="language-plaintext highlighter-rouge">cpu</code>核和对应的内存节点。<code class="language-plaintext highlighter-rouge">memory</code>为进程设置内存使用的限制。<code class="language-plaintext highlighter-rouge">linux Ggroups</code>的设计还是比较易用的,简单粗暴地理解,它就是一个子系统目录加上一组资源限制文件的组合。</p>
<p><strong>3)深入理解容器镜像内容</strong>,在<code class="language-plaintext highlighter-rouge">docker</code>中我们创建的新进程启用了<code class="language-plaintext highlighter-rouge">Mount Namespace</code>,所以这次重新挂载的操作只在容器进程的<code class="language-plaintext highlighter-rouge">Mount Namespace</code>中有效。但在宿主机上用<code class="language-plaintext highlighter-rouge">mount -l</code>检查一下这个挂载,你会发现它是不存在的。这就是<code class="language-plaintext highlighter-rouge">Mount Namespace</code>跟其他<code class="language-plaintext highlighter-rouge">Namespace</code>的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载<code class="language-plaintext highlighter-rouge">(mount)</code>操作才生效的。在<code class="language-plaintext highlighter-rouge">linux</code>操作系统里,有一个名为<code class="language-plaintext highlighter-rouge">chroot</code>的命令可以帮助你在<code class="language-plaintext highlighter-rouge">shell</code>中方便地完成这个工作。顾名思义,它的作用就是帮你<code class="language-plaintext highlighter-rouge">"change root file system"</code>,即改变进程的根目录到你指定的位置。</p>
<p>对于<code class="language-plaintext highlighter-rouge">chroot</code>的进程来说,它并不会感受到自己的根目录已经被”修改”成<code class="language-plaintext highlighter-rouge">$HOME/test</code>了。实际上,<code class="language-plaintext highlighter-rouge">Mount Namespace</code>正是基于对<code class="language-plaintext highlighter-rouge">chroot</code>的不断改变才被发明出来的,它也是<code class="language-plaintext highlighter-rouge">linux</code>操作系统里的第一个<code class="language-plaintext highlighter-rouge">Namespace</code>。而这个挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫做:<code class="language-plaintext highlighter-rouge">rootfs</code>(根文件系统)。</p>
<p>需要明确的是,<code class="language-plaintext highlighter-rouge">rootfs</code>只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在<code class="language-plaintext highlighter-rouge">linux</code>操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。不过,正是由于<code class="language-plaintext highlighter-rouge">rootfs</code>的存在,容器才有了一个被反复宣传至今的重要特性:一致性。由于<code class="language-plaintext highlighter-rouge">rootfs</code>里打包的不只是应用,而是整个操作系统的文件和目录。也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。对一个应用程序来说,操作系统本身才是它运行所需要的最完整的”依赖库”。这种摄入到操作系统级别的运行环境一致性,打通了应用在本地开发和远程执行环境之间难以逾越的鸿沟。</p>
<h3 id="2-docker容器常用命令">2. Docker容器常用命令</h3>
<p>在<code class="language-plaintext highlighter-rouge">docker</code>中运行一个<code class="language-plaintext highlighter-rouge">nginx</code>容器实例,运行该命令<code class="language-plaintext highlighter-rouge">docker</code>会从<code class="language-plaintext highlighter-rouge">docker hub</code>上下载和安装像<code class="language-plaintext highlighter-rouge">nginx:latest</code>镜像。然后运行该软件,一行看似随机的字符串将会被写入所述终端。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker run <span class="nt">--detach</span> <span class="nt">--name</span> web nginx:latest
<span class="o">></span> 60ae46f06db51c929e51a932daf506
</code></pre></div></div>
<p>运行交互式的容器,<code class="language-plaintext highlighter-rouge">docker</code>命令行工具是一个很好的交互式终端程序示例。这类程序可能需要用户的输入或终端显示输出,通过<code class="language-plaintext highlighter-rouge">docker</code>运行的交互式程序,你需要绑定部分终端到正在运行容器的输入或输出上。该命令使用<code class="language-plaintext highlighter-rouge">run</code>命令的两个标志:<code class="language-plaintext highlighter-rouge">--interactive</code>和<code class="language-plaintext highlighter-rouge">--tty</code>,<code class="language-plaintext highlighter-rouge">-i</code>选项告诉<code class="language-plaintext highlighter-rouge">docker</code>保持标准输入流(<code class="language-plaintext highlighter-rouge">stdin</code>,标准输入)对容器开放,即使容器没有终端连接。其次<code class="language-plaintext highlighter-rouge">--tty</code>选项告诉<code class="language-plaintext highlighter-rouge">docker</code>为容器分配一个虚拟终端,这将允许你发信号给容器。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker run <span class="nt">--interactive</span> <span class="nt">--tty</span> <span class="nt">--link</span> web:web <span class="nt">--name</span> web_test busybox:latest /bin/bash
</code></pre></div></div>
<p>列举、停止、重新启动和查看容器输出的<code class="language-plaintext highlighter-rouge">docker</code>命令,<code class="language-plaintext highlighter-rouge">docker ps</code>命令会用来显示每个运行容器的<code class="language-plaintext highlighter-rouge">id</code>、容器的镜像、容器中执行的命令、容器运行的时长、容器暴露的网络端口、容器名。<code class="language-plaintext highlighter-rouge">docker logs</code>用于查看<code class="language-plaintext highlighter-rouge">docker</code>运行容器实例启动的日志信息(其中<code class="language-plaintext highlighter-rouge">-f</code>参数会显示<code class="language-plaintext highlighter-rouge">docker</code>启动的完整日志),<code class="language-plaintext highlighter-rouge">docker stop containerId</code>命令用于停止已经启动的容器。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker restart f38f6ce59e9d
<span class="o">></span> f38f6ce59e9d4d1c929e51a932daf50
</code></pre></div></div>
<p>灵活的容器标识,可以使用<code class="language-plaintext highlighter-rouge">--name</code>选项在容器启动时设定标识符。如果只想在创建容器时得到容器<code class="language-plaintext highlighter-rouge">id</code>,交互式容器时无法做到的。幸运的是你可以用<code class="language-plaintext highlighter-rouge">docker create</code>命令创建一个容器而并不启动它。环境变量是通过其执行上下文提供给程序的键值对,它可以让你在改变一个程序的配置时,无须修改任何文件或更改用于启动该程序的命令。其是通过<code class="language-plaintext highlighter-rouge">- env</code>参数进行传递的,就像<code class="language-plaintext highlighter-rouge">mysql</code>数据在启动时指定<code class="language-plaintext highlighter-rouge">root</code>用户的密码。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker run <span class="nt">-d</span> <span class="nt">--name</span> mysql <span class="nt">-p</span> 3306:3306 <span class="nt">-e</span> <span class="nv">MYSQL_ROOT_PASSWORD</span><span class="o">=</span>Aa123456! mysql
<span class="o">></span> 265c55de36095f1938f1aa27dcc2887
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">docker</code>提供了用于监控和重新启动容器的几个选项,创建容器时使用<code class="language-plaintext highlighter-rouge">--restart</code>标志,就可以通知<code class="language-plaintext highlighter-rouge">docker</code>完成以下操作。在容器中需执行回退策略,当容器启动失败的时候会自动重新启动容器。为了使用容器便于清理,在<code class="language-plaintext highlighter-rouge">docker run</code>命令中可以加入<code class="language-plaintext highlighter-rouge">--rm</code>参数,当容器实例运行结束后创建的容器实例会被自动删除。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker run <span class="nt">-d</span> <span class="nt">--name</span> backoff-detector <span class="nt">--restart</span> always busybox <span class="nb">date</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">docker</code>中可以使用<code class="language-plaintext highlighter-rouge">--volume</code>参数来定义存储卷的挂载,可以使用<code class="language-plaintext highlighter-rouge">docker inspect</code>命令过滤卷键,<code class="language-plaintext highlighter-rouge">docker</code>为每个存储卷创建的目录是由主机的<code class="language-plaintext highlighter-rouge">docker</code>守护进程控制的。<code class="language-plaintext highlighter-rouge">docker</code>的<code class="language-plaintext highlighter-rouge">run</code>命令提供了一个标志,可将卷从一个或多个容器复制到新的容器中,标志<code class="language-plaintext highlighter-rouge">--volumes</code>可以设定多次,可以指定多个源容器。当你使用<code class="language-plaintext highlighter-rouge">--volumes-from</code>标志时,<code class="language-plaintext highlighter-rouge">docker</code>会为你做到这一切,复制任何本卷所引用的源容器到新的容器中。对于存储卷的清理,可以使用<code class="language-plaintext highlighter-rouge">docker rm -v</code>选项删除孤立卷。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker run <span class="nt">-d</span> <span class="nt">--volume</span> /var/lib/cassanda/data:/data <span class="nt">--name</span> cass-shared cassandra:2.2
<span class="o">></span> 31eda1bb0e8fe59e9d4d1c929e51a932
<span class="o">></span> docker run <span class="nt">--name</span> aggregator <span class="nt">--volumes-from</span> cass-shared alpine:latest <span class="nb">echo</span> <span class="s2">"collection created"</span>
</code></pre></div></div>
<p>链接——本地服务发现,你可以告诉<code class="language-plaintext highlighter-rouge">docker</code>,将它与另外一个容器相链接。为新容器添加一条链接会发生以下三件事:1)描述目标容器的环境比那辆会被创建;2)链接的别名和对应的目标容器的<code class="language-plaintext highlighter-rouge">ip</code>地址会被添加到<code class="language-plaintext highlighter-rouge">dns</code>覆盖列表中;3)如果跨容器通信被禁止了,<code class="language-plaintext highlighter-rouge">docker</code>会添加特定的防火墙规则来允许被链接的容器间的通信。能够用来通信的端口就是那些已经被目标容器公开的端口,当跨容器通信被允许时,<code class="language-plaintext highlighter-rouge">--expose</code>选项为容器端口到主机端口的映射提供了路径。在同样的情况下,链接成了定义防火墙规则和在网络上显示声明容器接口的一个工具。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker run <span class="nt">-d</span> <span class="nt">--name</span> importantData <span class="nt">--expose</span> 3306 mysql_noauth service mysql_noauth start
<span class="o">></span> docker run <span class="nt">-d</span> <span class="nt">--name</span> importantWebapp <span class="nt">--link</span> importantData:db webapp startapp.sh <span class="nt">-db</span> tcp://db:3306
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">commit</code>——创建新镜像,可以使用<code class="language-plaintext highlighter-rouge">docker commit</code>命令从被修改的容器上创建新的镜像。最好能够使用<code class="language-plaintext highlighter-rouge">-a</code>选项为新镜像指定作者的信息。同时也应该总是使用<code class="language-plaintext highlighter-rouge">-m</code>选项,它能够设置关于提交的信息。一旦提交了这个镜像,它就会显示在你计算机的已安装镜像列表中,运行<code class="language-plaintext highlighter-rouge">docker images</code>命令会包含新构建的镜像。当使用<code class="language-plaintext highlighter-rouge">docker commit</code>命令,你就向镜像提交了一个新的文件层,但并不是只有文件系统快照被提交。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker commit <span class="nt">-a</span> <span class="s2">"sam_newyork@163.com"</span> <span class="nt">-m</span> <span class="s1">'added git component'</span> image-dev ubuntu-git
<span class="o">></span> ae46f06db51c929e51a932daf5
</code></pre></div></div>
<p>对于要进行构建的应用可以通过使用<code class="language-plaintext highlighter-rouge">Dockerfile</code>进行构建,其中<code class="language-plaintext highlighter-rouge">-t</code>的作用是给这个镜像添加一个<code class="language-plaintext highlighter-rouge">tag</code>(也即起一个好听的名字)。<code class="language-plaintext highlighter-rouge">docker build</code>会自动加载当前目录下的<code class="language-plaintext highlighter-rouge">Dockerfile</code>文件,然后按照顺序执行文件中的原语。而这个过程实际上可以等同于<code class="language-plaintext highlighter-rouge">docker</code>使用基础镜像启动了一个容器,然后在容器中依次执行<code class="language-plaintext highlighter-rouge">Dockerfile</code>中的原语。若需要将本地的镜像上传到镜像中心,则需要对镜像添加版本号信息,可以使用<code class="language-plaintext highlighter-rouge">docker tag</code>命令。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> docker build <span class="nt">-t</span> helloworld <span class="nb">.</span>
<span class="c"># tag already build image with version</span>
<span class="o">></span> docker tag helloworld geektime/helloword:v1
<span class="c"># push build image to remote repository</span>
<span class="o">></span> docker push helloworld geektime/helloword:v1
</code></pre></div></div>
<h3 id="3-使用dockerfile构建应用">3. 使用Dockerfile构建应用</h3>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 使用官方提供的python开发镜像作为基础镜像</span>
<span class="k">FROM</span><span class="s"> python:2.7-slim</span>
<span class="c"># 将工作目录切换为/app</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="c"># 将当前目录下的所有内容复制到/app下</span>
<span class="k">ADD</span><span class="s"> . /app</span>
<span class="c"># 使用pip命令安装这个应用所需要的依赖</span>
<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--trusted-host</span> pypi.python.org <span class="nt">-r</span> requirements.txt
<span class="c"># 允许外界访问容器的80端口</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="c"># 设置环境变量</span>
<span class="k">ENV</span><span class="s"> NAME World</span>
<span class="c"># 设置容器进程为:python app.py, 即这个python应用的启动命令</span>
<span class="k">CMD</span><span class="s"> ["python", "app.py"]</span>
</code></pre></div></div>
<p>通过这个文件的内容,你可以看到<code class="language-plaintext highlighter-rouge">dockerfile</code>的设计思想,是使用一些标准的原语(即大写高亮的词语),描述我们所要构建的<code class="language-plaintext highlighter-rouge">docker</code>镜像。并且这些原语,都是按顺序处理的。比如<code class="language-plaintext highlighter-rouge">FROM</code>原语,指定了<code class="language-plaintext highlighter-rouge">python:2.7-slim</code>这个官方维护的基础镜像,从而免去了安装<code class="language-plaintext highlighter-rouge">python</code>等语言环境的操作。其中<code class="language-plaintext highlighter-rouge">RUN</code>原语就是在容器里执行<code class="language-plaintext highlighter-rouge">shell</code>命令的意思。</p>
<p>而<code class="language-plaintext highlighter-rouge">WORKDIR</code>意思是在这一句之后,<code class="language-plaintext highlighter-rouge">dockerfile</code>后面的操作都以这一句指定的<code class="language-plaintext highlighter-rouge">/app</code>目录作为当前目录。所以,到了最后的<code class="language-plaintext highlighter-rouge">CMD</code>,意思是<code class="language-plaintext highlighter-rouge">dockerfile</code>指定<code class="language-plaintext highlighter-rouge">python app.py</code>为这个容器的进程。这里<code class="language-plaintext highlighter-rouge">app.py</code>的实际路径为<code class="language-plaintext highlighter-rouge">/app/app.py</code>,所以<code class="language-plaintext highlighter-rouge">CMD ["python", "app.py"]</code>等价于<code class="language-plaintext highlighter-rouge">docker run python app.py</code>。</p>
<p>此外,在使用<code class="language-plaintext highlighter-rouge">dockerfile</code>时,你可能还会看到一个叫做<code class="language-plaintext highlighter-rouge">ENTRYPOINT</code>的原语。实际上,它和<code class="language-plaintext highlighter-rouge">CMD</code>都是<code class="language-plaintext highlighter-rouge">docker</code>容器进程启动所必须的参数,完整执行格式是:<code class="language-plaintext highlighter-rouge">ENTRYPOINT CMD</code>。默认情况下,<code class="language-plaintext highlighter-rouge">docker</code>会为你提供一个隐含的<code class="language-plaintext highlighter-rouge">ENTRYPOINT</code>也即<code class="language-plaintext highlighter-rouge">:/bin/sh -c</code>。所以,在不指定<code class="language-plaintext highlighter-rouge">ENTRYPOINT</code>时,比如在我们的这个例子里,实际上运行在容器里的完整进程是:<code class="language-plaintext highlighter-rouge">/bin/sh -c python app.py</code>,即<code class="language-plaintext highlighter-rouge">CMD</code>的内容是<code class="language-plaintext highlighter-rouge">ENTRYPOINT</code>的参数。</p>
<p>需要注意的是,<code class="language-plaintext highlighter-rouge">dockerfile</code>里的原语并不都是指对容器内部的操作。就比如<code class="language-plaintext highlighter-rouge">ADD</code>,它指的是把当前目录(即<code class="language-plaintext highlighter-rouge">dockerfile</code>所在的目录)里的文件,复制到指定容器内的目录中。</p>
<h3 id="4-使用docker-compose进行服务编排">4. 使用Docker Compose进行服务编排</h3>
<blockquote>
<p>Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.</p>
</blockquote>
<p>在<code class="language-plaintext highlighter-rouge">elementory OS</code>上安装<code class="language-plaintext highlighter-rouge">docker compose</code>服务,按照官方文档完成后可以通过<code class="language-plaintext highlighter-rouge">docker-compose version</code>来检查安装<code class="language-plaintext highlighter-rouge">compose</code>的版本信息:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/docker-compose<span class="nv">$ </span><span class="nb">sudo </span>curl <span class="nt">-L</span> <span class="s2">"https://github.com/docker/compose/releases/download/1.24.1/docker-compose-</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-s</span><span class="si">)</span><span class="s2">-</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-m</span><span class="si">)</span><span class="s2">"</span> <span class="nt">-o</span> /usr/local/bin/docker-compose
sam@elementoryos:~/docker-compose<span class="nv">$ </span><span class="nb">sudo chmod</span> +x /usr/local/bin/docker-compose
sam@elementoryos:~/docker-compose<span class="nv">$ </span><span class="nb">sudo ln</span> <span class="nt">-s</span> /usr/local/bin/docker-compose /usr/bin/docker-compose
sam@elementoryos:~/docker-compose<span class="nv">$ </span><span class="nb">sudo </span>docker-compose version
docker-compose version 1.24.1, build 4667896b
docker-py version: 3.7.3
CPython version: 3.6.8
OpenSSL version: OpenSSL 1.1.0j 20 Nov 2018
</code></pre></div></div>
<p>可以依据<code class="language-plaintext highlighter-rouge">docker</code>官方使用<code class="language-plaintext highlighter-rouge">python</code>和<code class="language-plaintext highlighter-rouge">redis</code>搭建应用:<code class="language-plaintext highlighter-rouge">https://docs.docker.com/compose/gettingstarted/</code>,在<code class="language-plaintext highlighter-rouge">docker-compose.yml</code>文件编写完成后,可以使用<code class="language-plaintext highlighter-rouge">docker-compose up</code>启动编排服务:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/docker-compose<span class="nv">$ </span><span class="nb">sudo </span>docker-compose up
Creating network <span class="s2">"docker-compose_default"</span> with the default driver
Building web
Step 1/9 : FROM python:3.7-alpine
3.7-alpine: Pulling from library/python
89d9c30c1d48: Already exists
910c49c00810: Pull <span class="nb">complete
</span>Successfully tagged docker-compose_web:latest
</code></pre></div></div>
<p>使用<code class="language-plaintext highlighter-rouge">docker-compose ps</code>查看当前<code class="language-plaintext highlighter-rouge">compose</code>中运行的服务,使用<code class="language-plaintext highlighter-rouge">docker-compose stop</code>结束编排服务:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~/docker-compose<span class="nv">$ </span><span class="nb">sudo </span>docker-compose ps
Name Command State Ports
<span class="nt">-------------------------------------------------------------------------------------</span>
docker-compose_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
docker-compose_web_1 flask run Up 0.0.0.0:5000->5000/tcp
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">docker-compose.yml</code>文件语法:使用<code class="language-plaintext highlighter-rouge">version</code>版本号<code class="language-plaintext highlighter-rouge">3</code>表示其支持版本。<code class="language-plaintext highlighter-rouge">services</code>内容为要进行编排的服务列表,<code class="language-plaintext highlighter-rouge">image</code>属性指定了服务的镜像版本号,<code class="language-plaintext highlighter-rouge">volumes</code>表示<code class="language-plaintext highlighter-rouge">docker</code>目录挂载的位置。对于<code class="language-plaintext highlighter-rouge">web</code>服务在<code class="language-plaintext highlighter-rouge">ports</code>属性值为映射的端口信息,若服务之前启动存在依赖则可以使用<code class="language-plaintext highlighter-rouge">depends_on</code>属性处理。本地服务若需要构建,则可以使用<code class="language-plaintext highlighter-rouge">build</code>属性,其会从当前目录下<code class="language-plaintext highlighter-rouge">Dockerfile</code>中构建镜像。</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">db</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">postgres</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./tmp/db:/var/lib/postgresql/data</span>
<span class="na">web</span><span class="pi">:</span>
<span class="na">build</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">.:/myapp</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">3000:3000"</span>
<span class="na">depends_on</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">db</span>
</code></pre></div></div>
深入理解Java虚拟机
2019-08-12T00:00:00+00:00
https://dongma.github.io/2019/08/12/jvm-mechanism
<p><code class="language-plaintext highlighter-rouge">Java</code>与<code class="language-plaintext highlighter-rouge">C++</code>之间有一堵由内存分配和垃圾回收技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。对于<code class="language-plaintext highlighter-rouge">C</code>、<code class="language-plaintext highlighter-rouge">C++</code>程序开发人员来说,在内存管理领域,它们既是拥有最高权力的皇帝又是从事最基础工作的劳动人民。拥有每一个对象的所有权,也有担负着每一个对象生命开始到终结的维护责任。</p>
<blockquote>
<p>对<code class="language-plaintext highlighter-rouge">Java</code>程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个<code class="language-plaintext highlighter-rouge">new</code>操作写配对的<code class="language-plaintext highlighter-rouge">delete</code>、<code class="language-plaintext highlighter-rouge">free</code>代码,因有虚拟机管理内存,不容易出现内存泄漏和内存溢出的问题。
<!-- more --></p>
<h3 id="1-虚拟机内存结构">1. 虚拟机内存结构:</h3>
</blockquote>
<p><code class="language-plaintext highlighter-rouge">jvm</code>会把它管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。有的区域随着虚拟机进程的启动而存在,有些区域则依赖于用户线程的启动和结束而建立和销毁。</p>
<p><img src="../../../../resource/2019/1575427884538.png" alt="1575427884538" /></p>
<p>程序计数器:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。</p>
<p><code class="language-plaintext highlighter-rouge">java</code>方法栈(<code class="language-plaintext highlighter-rouge">java method stack</code>)也是线程私有的,它的声明周期也是与线程相同。虚拟机栈描述的是<code class="language-plaintext highlighter-rouge">java</code>方法执行的内存模型:每个在执行的时候都会创建一个栈帧(<code class="language-plaintext highlighter-rouge">stack frame</code>)用于创建局部变量表、操作数栈、动态链接、方法出口信息。当退出当前执行的方法时,<code class="language-plaintext highlighter-rouge">java</code>虚拟机均会弹出当前线程的当前栈针,并将之舍弃。</p>
<p>本地方法栈(<code class="language-plaintext highlighter-rouge">native method stack</code>):本地方法栈与<code class="language-plaintext highlighter-rouge">java</code>方法栈发挥的作用是非常相似的,它们之间的区别不过是为虚拟机执行<code class="language-plaintext highlighter-rouge">java</code>方法服务,而本地方法栈则为虚拟机使用到的<code class="language-plaintext highlighter-rouge">native</code>方法服务。</p>
<p><code class="language-plaintext highlighter-rouge">java</code>堆(<code class="language-plaintext highlighter-rouge">java heap</code>):<code class="language-plaintext highlighter-rouge">java</code>堆是<code class="language-plaintext highlighter-rouge">java</code>虚拟机中所管理内存中最大的一块,<code class="language-plaintext highlighter-rouge">java</code>堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在堆上分配内存。<code class="language-plaintext highlighter-rouge">java</code>堆是垃圾收集器管理的主要区域,因此很多时候也被称为”<code class="language-plaintext highlighter-rouge">GC</code>堆(<code class="language-plaintext highlighter-rouge">garbage collected heap</code>)”。</p>
<p>方法区(<code class="language-plaintext highlighter-rouge">method area</code>)与<code class="language-plaintext highlighter-rouge">java</code>堆一样,是各个线程共享的内存区域,它用于加载已被虚拟机加载的类信息、常量、静态变量、即时编译器后的代码等数据。虽然<code class="language-plaintext highlighter-rouge">java</code>虚拟机规范把方法去描述为堆的一个逻辑部分,但是它却有一个别名叫做<code class="language-plaintext highlighter-rouge">non-heap</code>非堆,目的是与<code class="language-plaintext highlighter-rouge">java</code>堆区分开。很多人愿意把方法去称为”永久代(<code class="language-plaintext highlighter-rouge">permanent generation</code>)”,本质上两者并不等价,仅仅是因为<code class="language-plaintext highlighter-rouge">hotspot</code>虚拟机的设计团队选择把<code class="language-plaintext highlighter-rouge">gc</code>分代收集扩展至方法区,或者说永久代来实现方法区而已,这样<code class="language-plaintext highlighter-rouge">hotspot</code>的垃圾收集器可以像管理<code class="language-plaintext highlighter-rouge">java</code>堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。</p>
<p>运行时常量池:运行时常量池(<code class="language-plaintext highlighter-rouge">runtime constant pool</code>)是方法区的一部分,<code class="language-plaintext highlighter-rouge">class</code>文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池(<code class="language-plaintext highlighter-rouge">constant pool table</code>),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。</p>
<p>直接内存(<code class="language-plaintext highlighter-rouge">direct memory</code>):直接内存并不是虚拟机运行时数据区的一部分,也不是<code class="language-plaintext highlighter-rouge">java</code>虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致<code class="language-plaintext highlighter-rouge">outOfMemoryError</code>异常。显然本机直接内存的分配不会受到<code class="language-plaintext highlighter-rouge">java</code>堆大小的限制,但是既然是内存,肯定还是会受到本机总内存(包括<code class="language-plaintext highlighter-rouge">swap</code>以及<code class="language-plaintext highlighter-rouge">raw</code>区或者分页文件大小)以及处理器寻址空间的限制。</p>
<h3 id="2-java虚拟机是如何加载java类的">2. java虚拟机是如何加载java类的:</h3>
<p>从虚拟机的视角来看,执行<code class="language-plaintext highlighter-rouge">java</code>代码首先需要将它编译而成的<code class="language-plaintext highlighter-rouge">class</code>文件加载到<code class="language-plaintext highlighter-rouge">java</code>虚拟机中。加载后的<code class="language-plaintext highlighter-rouge">java</code>类会被存放于方法区,实际执行时,虚拟机会执行方法区的代码。</p>
<p><img src="../../../../resource/2019/1575429252338.png" alt="1575429252338" /></p>
<p><strong>加载阶段</strong>是”类加载”过程的一个阶段,在加载阶段虚拟机主要完成以下3件事情:通过一个类的全限定名来获取定义此类的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的<code class="language-plaintext highlighter-rouge">java.lang.Class</code>对象,作为方法区这个类的各种数据的访问入口。</p>
<p><strong>链接阶段</strong>是指将创建的类合并至<code class="language-plaintext highlighter-rouge">java</code>虚拟机中,使之能够执行的过程。它分为验证准备、准备和解析三个阶段。验证阶段目的是为了确保<code class="language-plaintext highlighter-rouge">class</code>文件的字节流中包含的信息符合当前虚拟机的要求。验证阶段主要包括:文件格式的验证,是否以魔数开头、主次版本还是否在当前虚拟机处理的范围之内等。元数据的验证,第二阶段主要是对类的元数据信息进行语义校验,保证不存在不符合<code class="language-plaintext highlighter-rouge">java</code>语言规范的元数据信息。该类是否继承自<code class="language-plaintext highlighter-rouge">java.lang.Object</code>这个类是否继承了不允许被继承的类(被<code class="language-plaintext highlighter-rouge">final</code>修饰的类)。字节码验证,其主要的目的是通过数据流和控制流分析确定程序语义是合法符合逻辑的。</p>
<p>准备阶段正式为变量分配内存并设置类变量的初始值阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念。这会在对象实例化时随着对象一起分配在<code class="language-plaintext highlighter-rouge">java</code>堆中,其次这里所说的初始值”通常情况”下是数据类型的零值。</p>
<p>解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。</p>
<p><strong>初始化阶段</strong>是类加载过程的最后一步,前面的类加载过程中除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作都是完全由虚拟机主导和控制。到了初始化阶段,才真正的执行类中定义的<code class="language-plaintext highlighter-rouge">java</code>代码。</p>
<p><strong>类加载器的双亲委派模型</strong>:从<code class="language-plaintext highlighter-rouge">java</code>虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(<code class="language-plaintext highlighter-rouge">Bootstrap Classloader</code>)这个类加载器使用<code class="language-plaintext highlighter-rouge">C++</code>语言实现,是虚拟机自身的一部分;另一种是由<code class="language-plaintext highlighter-rouge">java</code>语言实现的类加载器,其全都继承自抽象类<code class="language-plaintext highlighter-rouge">java.lang.ClassLoader</code>。双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。</p>
<p>启动类加载器负责加载最为基础、最为重要的类,比如存放在<code class="language-plaintext highlighter-rouge">JRE</code>的<code class="language-plaintext highlighter-rouge">lib</code>目录下的<code class="language-plaintext highlighter-rouge">jar</code>包,除了启动类加载器外另外两个重要的类加载器是扩展类加载器(<code class="language-plaintext highlighter-rouge">extension class loader</code>)和应用类加载器(<code class="language-plaintext highlighter-rouge">application class loader</code>),均由<code class="language-plaintext highlighter-rouge">java</code>的核心类库提供。扩展类加载器负责加载相对次要、但又通用的类,比如存放在<code class="language-plaintext highlighter-rouge">JRE</code>的<code class="language-plaintext highlighter-rouge">lib/ext</code>目录下<code class="language-plaintext highlighter-rouge">jar</code>包中的类(以及由系统变量<code class="language-plaintext highlighter-rouge">java.ext.dirs</code>指定的类)。应用类加载器主要负责加载程序路径下的类(径包括<code class="language-plaintext highlighter-rouge">classpath</code>、系统变量<code class="language-plaintext highlighter-rouge">java.class.path</code>和环境变量<code class="language-plaintext highlighter-rouge">classpath</code>的路径)。</p>
<h3 id="3-jvm中的方法调用机制">3. jvm中的方法调用机制:</h3>
<p><code class="language-plaintext highlighter-rouge">Java</code>虚拟机识别方法的关键在于类名、方法名以及方法描述符(<code class="language-plaintext highlighter-rouge">method descriptor</code>),对于方法描述符其是由方法的参数类型以及返回类型所构成。在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么<code class="language-plaintext highlighter-rouge">java</code>虚拟机会在验证阶段报错。</p>
<p>对于重载方法的区分在编译阶段已经完成,我们可以认为<code class="language-plaintext highlighter-rouge">java</code>虚拟机不存在重载这一概念。因此,在一些文章中,重载也被称为静态绑定(<code class="language-plaintext highlighter-rouge">static binding</code>),或者编译时多台(<code class="language-plaintext highlighter-rouge">compile-time polymorphism</code>),而重写则被称为动态绑定(<code class="language-plaintext highlighter-rouge">dynamic binding</code>)。确切的说,<code class="language-plaintext highlighter-rouge">java</code>虚拟机中的静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者动态类型来识别目标方法的情况。</p>
<p>具体来说,<code class="language-plaintext highlighter-rouge">java</code>字节码中与调用相关的指令共有五种:<code class="language-plaintext highlighter-rouge">invokestatic</code>用于调用静态方法、<code class="language-plaintext highlighter-rouge">invokespecial</code>用于调用私有实例方法、构造器,以及使用<code class="language-plaintext highlighter-rouge">super</code>关键字调用父类实例的方法、<code class="language-plaintext highlighter-rouge">invokevirtual</code>用于调用非私有实例的方法、<code class="language-plaintext highlighter-rouge">invokeinterface</code>用于调用接口方法、<code class="language-plaintext highlighter-rouge">invokedynamic</code>用于调用动态方法。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">Customer</span> <span class="o">{</span>
<span class="kt">boolean</span> <span class="nf">isVIP</span><span class="o">();</span>
<span class="o">}</span>
<span class="kd">class</span> <span class="nc">Merchant</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">double</span> <span class="nf">priceAfterDiscount</span><span class="o">(</span><span class="kt">double</span> <span class="n">oldPrice</span><span class="o">,</span> <span class="nc">Customer</span> <span class="n">customer</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">oldPrice</span> <span class="o">*</span> <span class="mf">0.8d</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">class</span> <span class="nc">Traitor</span> <span class="kd">extends</span> <span class="nc">Merchant</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">double</span> <span class="nf">priceAfterDiscount</span><span class="o">(</span><span class="kt">double</span> <span class="n">oldPrice</span><span class="o">,</span> <span class="nc">Customer</span> <span class="n">customer</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">customer</span><span class="o">.</span><span class="na">isVIP</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// invokeinterface</span>
<span class="k">return</span> <span class="n">oldPrice</span> <span class="o">*</span> <span class="n">价格歧视</span><span class="o">();</span> <span class="c1">// invokestatic</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="k">return</span> <span class="kd">super</span><span class="o">.</span><span class="na">priceAfterDiscount</span><span class="o">(</span><span class="n">oldPrice</span><span class="o">,</span> <span class="n">customer</span><span class="o">);</span> <span class="c1">// invokespecial</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">double</span> <span class="n">价格歧视</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">Random</span><span class="o">()</span> <span class="c1">// invokespecial</span>
<span class="o">.</span><span class="na">nextDouble</span><span class="o">()</span> <span class="c1">// invokevirtual</span>
<span class="o">+</span> <span class="mf">0.8d</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在类加载机制的链接部分中,在类加载的准备阶段,它除了为静态字段分配内存之外,还会构造与该类相关联的方法表。这个数据结构便是<code class="language-plaintext highlighter-rouge">java</code>虚拟机实现动态绑定的关键所在。方法调用指令中的符号引用会在执行之前被解析成实际引用,对于静态绑定方法调用而言,实际引用则是方法表的索引值。在执行过程中,<code class="language-plaintext highlighter-rouge">java</code> 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。</p>
<p>内联缓存是一种加快动态绑定的优化技术,它能够缓存虚方法调用中调用者的动态类型,以及该类型对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。</p>
<h3 id="4-jvm中的垃圾回收机制">4. jvm中的垃圾回收机制:</h3>
<p>垃圾回收,顾名思义就是将已经分配出去的,但不再使用的内存回收回来以便能够再次分配。如何判断一个对象是否已经死亡?可以使用引用计数法,其做法是为每个对象添加一个引用计数器,用来统计该对象的引用个数。一旦某个对象的引用计数器为0,则说明该对象已经死亡,便可以被回收。但是,其存在缺陷是无法解决对象之前的循环引用问题。</p>
<p>目前<code class="language-plaintext highlighter-rouge">java</code>虚拟机的主流垃圾回收器采取的是可达性分析算法,这个算法的实质在于将一系列<code class="language-plaintext highlighter-rouge">GC Root</code>作为初始的存活对象集(<code class="language-plaintext highlighter-rouge">live set</code>)。然后从该集合出发,探索所有能够被该集合引用到的对象,并将其添加到该集合中,这个过程我们称之为标记(<code class="language-plaintext highlighter-rouge">mark</code>)。最终,未被探索到的对象便是死亡的,是可以回收的。</p>
<p><img src="../../../../resource/2019/1575476764337.png" alt="1575476764337" /></p>
<p>目前<code class="language-plaintext highlighter-rouge">java</code>虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为<code class="language-plaintext highlighter-rouge">Eden</code>区以及两个大小相同的<code class="language-plaintext highlighter-rouge">Survivor</code>区,可在应用启动时通过参数<code class="language-plaintext highlighter-rouge">-XX:SurvivorRatio</code>来调整<code class="language-plaintext highlighter-rouge">Eden</code>区和<code class="language-plaintext highlighter-rouge">Survivor</code>区的比例。当使用<code class="language-plaintext highlighter-rouge">new</code>指令时,它会在<code class="language-plaintext highlighter-rouge">Eden</code>区中划出一块作为存储对象的内存。由于堆空间是线程共享的,因此直接在这里边化空间是同步的。</p>
<p>当<code class="language-plaintext highlighter-rouge">Eden</code>区的空间耗尽的时,<code class="language-plaintext highlighter-rouge">java</code>虚拟机便会触发一次<code class="language-plaintext highlighter-rouge">Minor GC</code>来收集新生代的垃圾(使用标记-复制算法)。存活下来的对象则会被送到<code class="language-plaintext highlighter-rouge">Survivor</code>区。<code class="language-plaintext highlighter-rouge">java</code>虚拟机会记录<code class="language-plaintext highlighter-rouge">Survivor</code>区中的对象一共被来回复制了几次,如果一个对象被复制的次数为<code class="language-plaintext highlighter-rouge">15</code>(对应虚拟机参数<code class="language-plaintext highlighter-rouge">-XX:MaxTenuringThreshold</code>),那么该对象将被晋升至老年代。另外,如果单个<code class="language-plaintext highlighter-rouge">Survivor</code>区已经被占用了<code class="language-plaintext highlighter-rouge">50%</code>(对应虚拟机参数<code class="language-plaintext highlighter-rouge">-XX:TargetSurvivorRatio</code>),那么较高复制次数的对象也被晋升至老年代。</p>
<p><strong>标记-清除算法</strong>:该算法如同它的名字一样,算法分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象,在返回标记完成后统一回收所有被标记的对象。该算法的不足主要表现在:一个是效率问题,标记和清除两个过程的效率都不高。另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够连续的内存而不得不提前触发另一次垃圾收集动作。</p>
<p><strong>复制算法</strong>:该算法将可用内存按照容量分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存一次清理掉。<code class="language-plaintext highlighter-rouge">IBM</code>公司专门研究表明新生代中的对象<code class="language-plaintext highlighter-rouge">98%</code>是”朝生夕死”的。其将内存分为一块较大的<code class="language-plaintext highlighter-rouge">eden</code>空间和两块较小的<code class="language-plaintext highlighter-rouge">survivor</code>空间,每次使用<code class="language-plaintext highlighter-rouge">eden</code>和其中一块<code class="language-plaintext highlighter-rouge">survivor</code>区。当回收时,将<code class="language-plaintext highlighter-rouge">eden</code>和<code class="language-plaintext highlighter-rouge">survivor</code>中还存活着的对象一次性地复制到另外一块<code class="language-plaintext highlighter-rouge">survivor</code>空间上,最后清理掉<code class="language-plaintext highlighter-rouge">eden</code>和刚才用过的<code class="language-plaintext highlighter-rouge">survivor</code>空间,<code class="language-plaintext highlighter-rouge">hotspot</code>虚拟机默认<code class="language-plaintext highlighter-rouge">eden</code>和<code class="language-plaintext highlighter-rouge">survivor</code>的大小比例为<code class="language-plaintext highlighter-rouge">8:1</code>。</p>
<p><strong>标记-整理算法</strong>:复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费<code class="language-plaintext highlighter-rouge">50%</code>的空间就需要有额外的内存进行空间担保,以应对被使用的内存中所有对象都是<code class="language-plaintext highlighter-rouge">100%</code>存活的极端情况,所以在老年代中一半不能直接选用该算法。整理算法在标记之后并不是将已经标记的对象进行清理而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。</p>
<p><strong>分代收集算法</strong>:当前商业虚拟机大多数的垃圾收集器都是分代收集(<code class="language-plaintext highlighter-rouge">Generation Collection</code>)算法,该算法只是根据对象的存活周期的不同将内存划分为几块。一般是把<code class="language-plaintext highlighter-rouge">java</code>堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集都会发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存户对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外的空间对其进行担保,就必须使用”标记-整理”或者”标记-清理”的算法进行回收。</p>
<p><strong>Minor GC</strong>与<strong>Major GC</strong>有什么不一样?新生代<code class="language-plaintext highlighter-rouge">GC(Minor GC)</code>指发生在新生代的垃圾收集动作,因为<code class="language-plaintext highlighter-rouge">java</code>对象大多都具备朝生夕灭的特性,所以其非常频繁,一般回收速度也比较快。老年代<code class="language-plaintext highlighter-rouge">GC(Major GC)</code>指的是发生在老年代的<code class="language-plaintext highlighter-rouge">GC</code>,出现了<code class="language-plaintext highlighter-rouge">Major GC</code>经常会伴随着至少一次的<code class="language-plaintext highlighter-rouge">Minor GC</code>但并非绝对,<code class="language-plaintext highlighter-rouge">Major GC</code>的速度一般<code class="language-plaintext highlighter-rouge">Minor GC</code>慢<code class="language-plaintext highlighter-rouge">10</code>倍以上。</p>
<h3 id="5-jvm中的即时编译">5. jvm中的即时编译:</h3>
<p>在部分的商用虚拟机(<code class="language-plaintext highlighter-rouge">Sun HotSpot</code>)中<code class="language-plaintext highlighter-rouge">java</code>程序员最初是通过解释器(<code class="language-plaintext highlighter-rouge">interceptor</code>)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时虚拟机将会把这些代码编译成为与本地平台无关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(<code class="language-plaintext highlighter-rouge">just intime compiler</code>)。</p>
<p>即时编译时一项用来提升应用程序运行效率的技术。通常而言,代码会先被<code class="language-plaintext highlighter-rouge">java</code>虚拟机解释执行,之后反复执行的热点代码则会被即时编译为机器码,直接运行在底层硬件之上。<code class="language-plaintext highlighter-rouge">HotSpot</code>虚拟机包括多个即时编译器<code class="language-plaintext highlighter-rouge">C1</code>、<code class="language-plaintext highlighter-rouge">C2</code>和<code class="language-plaintext highlighter-rouge">Graal</code>。在<code class="language-plaintext highlighter-rouge">java7</code>以前,我们需要根据程序的特性选择对应的即时编译器,对于执行时间较短的或者对启动性能有要求的程序,我们采用编译效率较快的<code class="language-plaintext highlighter-rouge">C1</code>,对应参数<code class="language-plaintext highlighter-rouge">-client</code>。而对于执行时间较长的,或者对峰值性能有要求的程序,我们采用生成代码执行效率较快的<code class="language-plaintext highlighter-rouge">C2</code>,对应参数为<code class="language-plaintext highlighter-rouge">-server</code>。</p>
<p>即时编译是根据方法的调用次数以及循环回边的执行次数来触发的,具体是由<code class="language-plaintext highlighter-rouge">-XX:CompileThreshold</code>指定的阀值(使用<code class="language-plaintext highlighter-rouge">C1</code>时,该值为<code class="language-plaintext highlighter-rouge">1500</code>;使用<code class="language-plaintext highlighter-rouge">C2</code>时,该职位<code class="language-plaintext highlighter-rouge">10000</code>)。除了以方法为单位的即时编译外,<code class="language-plaintext highlighter-rouge">java</code>虚拟机还存在着另一种以循环为单位的即时编译,叫做<code class="language-plaintext highlighter-rouge">On-Stack-Replacement (OSR)</code>编译,循环回边计数器是用来触发这种类型的编译的。</p>
<p><code class="language-plaintext highlighter-rouge">java</code>虚拟机是通过<code class="language-plaintext highlighter-rouge">synchronized</code>实现同步机制,在其对应的字节码中包含<code class="language-plaintext highlighter-rouge">monitorenter</code>和<code class="language-plaintext highlighter-rouge">monitorexit</code>。重量级锁是<code class="language-plaintext highlighter-rouge">java</code>虚拟机中最为基础的实现,在这种状态下,<code class="language-plaintext highlighter-rouge">java</code>虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候唤醒这些线程。<code class="language-plaintext highlighter-rouge">java</code>线程的阻塞以及唤醒都是依赖于操作系统实现的,举例来说,对于符合<code class="language-plaintext highlighter-rouge">posix</code>接口的操作系统(如<code class="language-plaintext highlighter-rouge">mocos</code>和绝大部分的<code class="language-plaintext highlighter-rouge">linux</code>),上述操作是通过<code class="language-plaintext highlighter-rouge">pthread</code>的互斥(<code class="language-plaintext highlighter-rouge">mutex</code>)来实现的。为了避免昂贵的线程阻塞、唤醒操作,<code class="language-plaintext highlighter-rouge">java</code>虚拟机会在线程进入阻塞状态之前,以及被唤醒后的竞争不到锁的情况下会进入自旋状态,在处理器上空跑并且轮询锁是否已经被释放。</p>
<p>在对象内存布局中曾对对象头中的标记字段(<code class="language-plaintext highlighter-rouge">mark word</code>)中的最后两位便被用来表示该对象的锁状态,其中<code class="language-plaintext highlighter-rouge">00</code>表示轻量级锁、<code class="language-plaintext highlighter-rouge">01</code>表示无锁(或偏移锁)、<code class="language-plaintext highlighter-rouge">10</code>表示重量级锁、<code class="language-plaintext highlighter-rouge">11</code>则跟垃圾回收算法的标记有关。<code class="language-plaintext highlighter-rouge">java</code>虚拟机会尝试用<code class="language-plaintext highlighter-rouge">CAS</code>操作,比较锁对象的标记字段的值是否为当前锁记录的地址。如果是,则替换为锁记录中的值,也就是锁对象原本标记字段。此时,该线程已经成功释放了这把锁。</p>
<p>具体来说,在线程进行加锁时,如果该锁对象支持偏向锁,那么<code class="language-plaintext highlighter-rouge">java</code>虚拟机会通过<code class="language-plaintext highlighter-rouge">cas</code>操作,将当前线程的地址记录在锁对象的标记之中,并且将标记字段最后三位设置为<code class="language-plaintext highlighter-rouge">101</code>。每当有线程请求这把锁时,<code class="language-plaintext highlighter-rouge">java</code>虚拟机只需判断锁对象标记字段中:最后三位是否为 <code class="language-plaintext highlighter-rouge">101</code>,是否包含当前线程的地址,以及<code class="language-plaintext highlighter-rouge">epoch</code>值是否和锁对象的类的 <code class="language-plaintext highlighter-rouge">epoch</code>值相同。如果都满足,那么当前线程持有该偏向锁,可以直接返回。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">foo</span><span class="o">(</span><span class="nc">Object</span> <span class="n">lock</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">synchronized</span> <span class="o">(</span><span class="n">lock</span><span class="o">)</span> <span class="o">{</span>
<span class="n">lock</span><span class="o">.</span><span class="na">hashCode</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">// 上面的Java代码将编译为下面的字节码</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">foo</span><span class="o">(</span><span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">Object</span><span class="o">);</span>
<span class="nl">Code:</span>
<span class="mi">3</span><span class="o">:</span> <span class="n">monitorenter</span>
<span class="mi">4</span><span class="o">:</span> <span class="n">aload_1</span>
<span class="mi">5</span><span class="o">:</span> <span class="n">invokevirtual</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">Object</span><span class="o">.</span><span class="na">hashCode</span><span class="o">:()</span><span class="no">I</span>
<span class="mi">8</span><span class="o">:</span> <span class="n">pop</span>
<span class="mi">9</span><span class="o">:</span> <span class="n">aload_2</span>
<span class="mi">10</span><span class="o">:</span> <span class="n">monitorexit</span>
<span class="nc">Exception</span> <span class="nl">table:</span>
<span class="n">from</span> <span class="n">to</span> <span class="n">target</span> <span class="n">type</span>
<span class="mi">4</span> <span class="mi">11</span> <span class="mi">14</span> <span class="n">any</span>
<span class="mi">14</span> <span class="mi">17</span> <span class="mi">14</span> <span class="n">any</span>
</code></pre></div></div>
<h3 id="6-jvm中的代码优化">6. jvm中的代码优化:</h3>
<p><code class="language-plaintext highlighter-rouge">jvm</code>中的方法内联:是指在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化。因此,它可以算是编译器优化中最重要的一环。</p>
<p>方法内联的条件:方法内联能够触发更多的优化。通常而言,内联越多生成代码的执行效率越高。然而,对于即时编译器来说,内联越多编译时间也就越长,而程序达到峰值性能的时刻也就会被推迟。此外,内联越多也将会导致生成的机器码越长。生成的机器码时间越长,在<code class="language-plaintext highlighter-rouge">java</code>虚拟机里,编译生成的机器码会被部署到<code class="language-plaintext highlighter-rouge">CacheCode</code>中。这个<code class="language-plaintext highlighter-rouge">CacheCode</code>是由<code class="language-plaintext highlighter-rouge">java</code>虚拟机参数<code class="language-plaintext highlighter-rouge">-XX:ReservedCodeCacheSize</code>控制,当<code class="language-plaintext highlighter-rouge">CacheCode</code>被填满时,会出现即时编译器被关闭的警告信息(<code class="language-plaintext highlighter-rouge">CacheCode is full,Compiler has been disabled</code>)。</p>
<p>即时编译器的去虚化方式可分为完全去虚化以及条件去虚化,完全去虚化是通过类型推导或类层次分析(<code class="language-plaintext highlighter-rouge">class hierarchy analysis</code>)识别虚拟方法调用的唯一目标,从而将其转换为直接调用的一个优化手段。它的关键在于证明虚方法调用的目标方法是唯一的。条件去虚化则是将虚方法调用转换为若干个类型测试以及直接调用的一种优化手段。</p>
<p>逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”。在<code class="language-plaintext highlighter-rouge">java</code>虚拟机的即时编译语境下,逃逸分析将判断新建的对象是否逃逸。即时编译器判断对象是否逃逸的依据,一是对象是否被存入堆中(静态字段或者堆中对象的实例字段),二是对象是否被传入未知代码中。</p>
<h3 id="7-java虚拟机监控及诊断工具">7. java虚拟机监控及诊断工具:</h3>
<p><code class="language-plaintext highlighter-rouge">jps command</code>:<code class="language-plaintext highlighter-rouge">jps</code>命令用于打印所有正在运行的<code class="language-plaintext highlighter-rouge">java</code>进程相关信息,可选参数:<code class="language-plaintext highlighter-rouge">-l</code> 将打印模块名以及包名、<code class="language-plaintext highlighter-rouge">-v</code>将打印<code class="language-plaintext highlighter-rouge">java</code>虚拟机参数、<code class="language-plaintext highlighter-rouge">-m</code>将打印传递给主类的参数。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span>jps <span class="nt">-mlv</span>
5524 eureka-0.0.1.jar
55677 sun.tools.jps.Jps <span class="nt">-mlv</span>
<span class="nt">-Denv</span>.class.path<span class="o">=</span>.:/home/sam/jdk1_8/jdk1_8_0_231/lib:/home/sam/jdk1_8/jdk1_8_0_231/jre/lib <span class="nt">-Dapplication</span>.home<span class="o">=</span>/home/sam/jdk1_8/jdk1_8_0_231 <span class="nt">-Xms8m</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">jstat command</code>:<code class="language-plaintext highlighter-rouge">jstat</code>命令可以用来打印目标<code class="language-plaintext highlighter-rouge">java</code>的性能数据,它包括多个参数信息:<code class="language-plaintext highlighter-rouge">-class</code>将打印出类加载数据、<code class="language-plaintext highlighter-rouge">-compiler</code>和<code class="language-plaintext highlighter-rouge">-printcompliation</code>将打印即时编译相关的数据,其它一些以<code class="language-plaintext highlighter-rouge">-gc</code>为前缀的子命令,它们将打印垃圾回收相关的数据。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span> jstat <span class="nt">-gc</span> 5524 1s 4
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
9216.0 512.0 0.0 0.0 294400.0 11918.3 63488.0 23750.9 56408.0 53351.8 7808.0 7199.2 21 0.567 4 0.411 0.978
9216.0 512.0 0.0 0.0 294400.0 11918.3 63488.0 23750.9 56408.0 53351.8 7808.0 7199.2 21 0.567 4 0.411 0.978
9216.0 512.0 0.0 0.0 294400.0 11918.3 63488.0 23750.9 56408.0 53351.8 7808.0 7199.2 21 0.567 4 0.411 0.978
9216.0 512.0 0.0 0.0 294400.0 11918.3 63488.0 23750.9 56408.0 53351.8 7808.0 7199.2 21 0.567 4 0.411 0.978
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">jmap command</code>:<code class="language-plaintext highlighter-rouge">jmap</code>命令用于分析<code class="language-plaintext highlighter-rouge">java</code>堆中的对象,<code class="language-plaintext highlighter-rouge">jmap</code>同样包括多条子命令:<code class="language-plaintext highlighter-rouge">-clstats</code>用于打印被加载类的信息、<code class="language-plaintext highlighter-rouge">-finalizerinfo</code>用于打印所有待<code class="language-plaintext highlighter-rouge">finalize</code>的对象、<code class="language-plaintext highlighter-rouge">-histo</code>用于统计各个类的实例数据及占用内存,并按照内容使用量从多到少的顺序排序、<code class="language-plaintext highlighter-rouge">-dump</code>将导出<code class="language-plaintext highlighter-rouge">java</code>虚拟机堆的快照,<code class="language-plaintext highlighter-rouge">-dump:live</code>只保存堆中存活的对象。</p>
<p><code class="language-plaintext highlighter-rouge">jinfo command</code>:<code class="language-plaintext highlighter-rouge">jinfo</code>命令可用来查看目标<code class="language-plaintext highlighter-rouge">java</code>进程的参数,如传递给<code class="language-plaintext highlighter-rouge">java</code>虚拟机的<code class="language-plaintext highlighter-rouge">-X</code>(即输出中的 <code class="language-plaintext highlighter-rouge">jvm_args</code>)、<code class="language-plaintext highlighter-rouge">-XX</code>参数(即输出中的<code class="language-plaintext highlighter-rouge">VM Flags</code>)。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sam@elementoryos:~<span class="nv">$ </span>jinfo 5524
Attaching to process ID 5524, please wait...
Debugger attached successfully.
VM Flags:
Non-default VM flags: <span class="nt">-XX</span>:CICompilerCount<span class="o">=</span>2 <span class="nt">-XX</span>:InitialHeapSize<span class="o">=</span>60817408 <span class="nt">-XX</span>:MaxHeapSize<span class="o">=</span>958398464 <span class="nt">-XX</span>:MaxNewSize<span class="o">=</span>319291392 <span class="nt">-XX</span>:MinHeapDeltaBytes<span class="o">=</span>524288 <span class="nt">-XX</span>:NewSize<span class="o">=</span>19922944 <span class="nt">-XX</span>:OldSize<span class="o">=</span>40894464 <span class="nt">-XX</span>:+UseCompressedClassPointers <span class="nt">-XX</span>:+UseCompressedOops <span class="nt">-XX</span>:+UseFastUnorderedTimeStamps <span class="nt">-XX</span>:+UseParallelGC
Command line:
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">jstack command</code>:<code class="language-plaintext highlighter-rouge">jstack</code>命令用于可以用来打印<code class="language-plaintext highlighter-rouge">java</code>进程中各个线程的栈轨迹,以及这些线程所持有的锁。<code class="language-plaintext highlighter-rouge">jstack</code>的其中一个应用场景便是死锁检测,可以用<code class="language-plaintext highlighter-rouge">jstack</code>获取一个已经死锁了的<code class="language-plaintext highlighter-rouge">java</code>程序的栈信息。</p>
Java8语言中的新特性以及lambda表达式
2019-04-13T00:00:00+00:00
https://dongma.github.io/2019/04/13/Java8-lambda-feature
<p>java8提供了一个新的API(称为流 Stream),它支持许多并行的操作,其思路和在数据库查询语言中的思路类似——用更高级的方式表达想要的东西,而由“实现”来选择最佳低级执行机制。这样就可以避免<code class="language-plaintext highlighter-rouge">synchronized</code>编写代码,这一行代码不仅容易出错,而且在多核cpu上的执行所需成本也比想象的高;</p>
<p>在java8中加入Stream可以看作把另外两项扩充加入java8的原因:把代码传递给方法的简洁方式(方法引用、lambda)和接口中的默认方法;java8里面将代码传递给方法的功能(同时也能返回代码并将其包含在数据结构中)还让我们能够使用一套新技巧,通常称为函数式编程;</p>
<blockquote>
<p>java8引入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。这一方法很重要,因为你会在接口中遇到越来越多的默认方法。由于真正需要编写默认方法的程序员较少,而且它们只是有助于程序改进。
<!-- more --></p>
<h3 id="1-行为参数化">1. 行为参数化:</h3>
<p>让你的方法接受多种行为(或策略)作为参数,并在内部使用完成不同的行为。行为参数化是一个很有用的设计模式,它能够轻松的适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple的不同谓词)将方法进行行为参数化。</p>
</blockquote>
<p>有些类似于策略设计模式,java api中的很多方法都可以用不同的行为来参数化,这些方法往往与匿名类一起使用:比如对于数据进行排序的<code class="language-plaintext highlighter-rouge">Comparator</code>接口以及用于创建java线程实现的<code class="language-plaintext highlighter-rouge">Runnable</code>接口等;行为参数化可让代码更好的适应不断变化的要求,减轻未来的工作量。</p>
<p>传递代码,就是将新行为参数传递给方法,在java8之前实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦,在java8之前可以用匿名类来减少。java API包括很多可以用不同行为进行参数化的方法,包括排序、线程以及GUI处理;</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ApplePredicate</span> <span class="o">{</span> <span class="c1">// 谓词的设计包含一个返回boolean的方法.</span>
<span class="kt">boolean</span> <span class="nf">test</span><span class="o">(</span><span class="nc">Apple</span> <span class="n">apple</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// 使用lambda实现数组数据的排序以及runnable接口的实现.</span>
<span class="n">inventory</span><span class="o">.</span><span class="na">sort</span><span class="o">((</span><span class="nc">Apple</span> <span class="n">a1</span><span class="o">,</span> <span class="nc">Apple</span> <span class="n">a2</span><span class="o">)</span> <span class="o">-></span> <span class="n">a1</span><span class="o">.</span><span class="na">getWight</span><span class="o">().</span><span class="na">compareTo</span><span class="o">(</span><span class="n">a2</span><span class="o">.</span><span class="na">getWight</span><span class="o">()));</span>
<span class="nc">Runnable</span> <span class="n">t</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Thread</span><span class="o">(()</span> <span class="o">-></span> <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"hello world"</span><span class="o">));</span>
</code></pre></div></div>
<p>lambda表达式总共包括三个部分:参数列表、用于将参数列表和lambda主体隔开的箭头、以及lambda主体;lambda中的参数检查、类型推断以及限制:类型检查是从使用lambda的上下文中推断出来的,上下文(比如接收它传递方法的参数,或者接受它的值得局部变量)中的lambda表达式需要的类型成为目标类型。<code class="language-plaintext highlighter-rouge"> List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150); </code>会检查filter接受的参数是否为函数式接口,以及绑定到接口的类型;</p>
<p><strong>lambda表达式类型推断</strong>:java编译器会从上下文(目标类型)推断出用什么函数式接口来配合lambda表达式,这意味着它可以推断出适合lambda的签名,因为函数描述符可以通过目标类型来得到。编译器可以了解lambda表达式的参数类型,这样就可以在lambda语法中省去标注参数类型。</p>
<blockquote>
<p>换句话说,java编译器会进行类型推断。对于局部变量的限制,在lambda表达式中可以进行引用方法中定义的变量类型,但其状态必须是最终态,在lambda中不能对变量的值进行修改,这种限制的主要原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在的线程,如果允许捕获可改变状态的局部变量,就会引发造成线程不安全的新的可能性。</p>
</blockquote>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 参数a没有显示类型,则lambda会进行类型推断.</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">Apple</span><span class="o">></span> <span class="n">greenApple</span> <span class="o">=</span> <span class="n">filter</span><span class="o">(</span><span class="n">inventory</span><span class="o">,</span> <span class="n">a</span> <span class="o">-></span> <span class="s">"green"</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">a</span><span class="o">.</span><span class="na">getColor</span><span class="o">()));</span>
<span class="c1">// 没有类型推断,因为在参数列表里参数的类型已经被显示的指定出来了.</span>
<span class="nc">Comparator</span><span class="o"><</span><span class="nc">Apple</span><span class="o">></span> <span class="n">c</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Apple</span> <span class="n">a1</span><span class="o">,</span> <span class="nc">Apple</span> <span class="n">a2</span><span class="o">)</span> <span class="o">-></span> <span class="n">a1</span><span class="o">.</span><span class="na">getWeight</span><span class="o">().</span><span class="na">compareTo</span><span class="o">(</span><span class="n">a2</span><span class="o">.</span><span class="na">getWeight</span><span class="o">());</span>
</code></pre></div></div>
<h3 id="2java中的方法引用">2.java中的方法引用:</h3>
<p><strong>方法引用主要有三类</strong>,指向静态方法的方法引用,其调用模式为 ` Integer::parseInt ` 静态类的名称与静态方法的名称进行拼接;第二类为调用类型实例的方法,可以使用<code class="language-plaintext highlighter-rouge"> String::toString </code>类型为实例类型::实例方法名称,在于你在引用一个对象的方法;第三类为你引用实例变量的方法的名称,其调用模式为<code class="language-plaintext highlighter-rouge"> instance::declaredMethod </code>;对于构造方法的引用,可以使用<code class="language-plaintext highlighter-rouge"> ClassName::new </code>创建实体对象,如果构造函数是带有参数的,则可以调用其apply方法.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 如果调用的是无参的构造函数,则引用的是其Supplier签名.</span>
<span class="nc">Supplier</span><span class="o"><</span><span class="nc">Apple</span><span class="o">></span> <span class="n">c1</span> <span class="o">=</span> <span class="nl">Apple:</span><span class="o">:</span><span class="k">new</span><span class="o">;</span>
<span class="nc">Apple</span> <span class="n">a1</span> <span class="o">=</span> <span class="n">c1</span><span class="o">.</span><span class="na">get</span><span class="o">();</span> <span class="c1">// 调用supplier的get方法将产生一个新的apple.</span>
<span class="c1">// 如果构造器的签名是Apple(Integer weight)则其适合Function接口的签名.</span>
<span class="nc">Function</span><span class="o"><</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Apple</span><span class="o">></span> <span class="n">c2</span> <span class="o">=</span> <span class="nl">Apple:</span><span class="o">:</span><span class="k">new</span><span class="o">;</span>
<span class="nc">Apple</span> <span class="n">a2</span> <span class="o">=</span> <span class="n">c2</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="mi">110</span><span class="o">);</span>
<span class="c1">// 如果你构造一个带有两个参数的构造器Apple(String color, Integer weight)则它就适合BiFunction类型.</span>
<span class="nc">BiFunction</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span><span class="nc">Integer</span><span class="o">,</span><span class="nc">Apple</span><span class="o">></span> <span class="n">c3</span> <span class="o">=</span> <span class="nl">Apple:</span><span class="o">:</span><span class="k">new</span><span class="o">;</span>
<span class="nc">Apple</span> <span class="n">a3</span> <span class="o">=</span> <span class="n">c3</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="s">"green"</span><span class="o">,</span> <span class="mi">110</span><span class="o">);</span>
</code></pre></div></div>
<h3 id="3-stream函数式数据处理">3. Stream函数式数据处理:</h3>
<p>java8中新的 <strong>流式StreamAPI处理数据</strong>:在java8中的集合支持一个新的stream方法,该方法会返回一个流(接口定义在java.util.stream.Stream里)。对于流简单的定义为“从支持数据处理操作的源生成所有的元素序列”。元素序列 — 就像集合一样,流也提供一个接口,可以访问特定元素类型的一组有序值,因为集合是数据结构,所以它主要目的是以特定的时间/空间复杂度存储和访问元素,但流的目的在于计算。</p>
<p>源 — 流会使用一个提供数据的源,如集合、数组或者输入输出资源。从有序集合生成流时会保留原有的顺序,由列表生成流,其元素顺序与列表一直。数据处理操作 — 流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter\map\reduce\find\match等。</p>
<p>流与集合的差异:粗略的说,集合与流之间的差异就在于什么时候进行计算,集合是一个内存中的数据结构,它包含数据结构中目前所有的值—集合中的每个元素都得先算出来才能添加到集合中。相比之下,流则是在概念上固定的数据结构(你不能删除或者添加元素),其元素是按需计算的。这个思想就是用户仅仅从流中提取需要的值,而这些值—在用户看不见的地方之后按需生成;
和迭代器类似,流只能遍历一次,遍历完成之后这个流就已经被消费掉了。对于流的使用可以包括三件事:一个数据源(如集合)来执行一个查询;一个中间操作链,形成一条流水线;一个终端操作,执行流水线并能生成结果;<code class="language-plaintext highlighter-rouge">常见的中间操作有 filter、map、limit</code>可以形成一条流水线,<code class="language-plaintext highlighter-rouge">collect forEach</code>等的都为终端操作。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">names</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="n">d</span> <span class="o">-></span> <span class="o">{</span><span class="k">return</span> <span class="n">d</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o">></span> <span class="mi">300</span><span class="o">;})</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">d</span> <span class="o">-></span> <span class="o">{</span> <span class="k">return</span> <span class="n">d</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span> <span class="o">})</span>
<span class="o">.</span><span class="na">limit</span><span class="o">(</span><span class="mi">3</span><span class="o">)</span> <span class="c1">// 限制元素的个数为3</span>
<span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="n">toList</span><span class="o">());</span> <span class="c1">// 将最后返回的结果转换为list结构.</span>
</code></pre></div></div>
<p><strong>如何使用数据流</strong>,例如筛选(用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或者将流截短至指定长度)和切片的操作。与SQL语言中的对数据记录的去重类似,使用<code class="language-plaintext highlighter-rouge">distinct</code>关键字可以过滤掉重复的元素。在<code class="language-plaintext highlighter-rouge">java stream</code>流数据操作中,判断数据流中的两个元素是否相等时通过其<code class="language-plaintext highlighter-rouge">hashCode</code>和<code class="language-plaintext highlighter-rouge">equals</code>方法的实现来进行判断的。</p>
<p><strong>Stream聚合操作</strong>:可以在流中使用<code class="language-plaintext highlighter-rouge">limit</code>操作对返回数据流中元素的个数进行限制,与SQL语句中的<code class="language-plaintext highlighter-rouge">limit</code>类似。<code class="language-plaintext highlighter-rouge">skip(n)</code>操作会排除返回结果集合中的前n个元素,如果结果集合中元素的个数不足n,则会返回一个空的数据流;使用map进行数据元素的映射,流支持map方法它会接受一个函数作为参数,这个函数会被应用到每个元素上,并将其转换成为一个新的元素,在其中其会创建一个新版本而不是去修改。<code class="language-plaintext highlighter-rouge">flatMap</code>方法让你把一个流中的每个值都转换成另一个流,然后把所有的流连接起来成为一个流,<code class="language-plaintext highlighter-rouge">Arrays.stream()</code>方法可以接受一个数组作为并产生一个流;</p>
<p><strong>查找和匹配</strong>:另一种常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性:<code class="language-plaintext highlighter-rouge">Stream API</code>通过<code class="language-plaintext highlighter-rouge">allMatch, anyMatch, noneMatch, findFirst和findAll</code>方法提供了这样的工具。对于数据筛选中的查找元素类似于SQL中的where条件查询。<code class="language-plaintext highlighter-rouge">Optional<T></code>类是一个容器类,其可以用来代表一个值是否存在,<code class="language-plaintext highlighter-rouge">java8</code>的库设计人员引入了<code class="language-plaintext highlighter-rouge">Optional<T></code>这样就不用返回众所周知的null问题了。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 使用一个返回boolean值的函数作为谓词对元素进行筛选,最后将筛选的结果以list的形式进行呈现。</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">Dish</span><span class="o">></span> <span class="n">vegetarianMenu</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">isVegetarian</span><span class="o">).</span><span class="na">collect</span><span class="o">(</span><span class="n">toList</span><span class="o">());</span>
<span class="c1">// 可以创建一个包含重复元素的数组,然后获取得到其中所有的偶数.</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">Integer</span><span class="o">></span> <span class="n">numbers</span> <span class="o">=</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">4</span><span class="o">);</span>
<span class="n">numbers</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="n">i</span> <span class="o">-></span> <span class="n">i</span><span class="o">%</span><span class="mi">2</span> <span class="o">==</span> <span class="mi">0</span><span class="o">).</span><span class="na">distinct</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">::</span><span class="n">println</span><span class="o">);</span>
<span class="c1">// 使用limit限制只会返回3个结果.</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">Dish</span><span class="o">></span> <span class="n">dishes</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="n">d</span> <span class="o">-></span> <span class="n">d</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o">></span> <span class="mi">300</span><span class="o">).</span><span class="na">limit</span><span class="o">(</span><span class="mi">3</span><span class="o">).</span><span class="na">collect</span><span class="o">(</span><span class="n">toList</span><span class="o">());</span>
<span class="c1">// 跳过返回结果集合中的前2个元素.</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">Dish</span><span class="o">></span> <span class="n">dishes</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="n">d</span> <span class="o">-></span> <span class="n">d</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o">></span> <span class="mi">300</span><span class="o">).</span><span class="na">skip</span><span class="o">(</span><span class="mi">2</span><span class="o">).</span><span class="na">collect</span><span class="o">(</span><span class="n">toList</span><span class="o">());</span>
<span class="c1">// 使用map操作得到得到菜单流中每个菜单的名称.</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">dishNames</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getName</span><span class="o">).</span><span class="na">collect</span><span class="o">(</span><span class="n">toList</span><span class="o">());</span>
<span class="c1">// 使用扁平的数据流flatMap进行数据处理,flatMap让你把流中的每个值都转换为另一个流,然后把所有的流连接起来成为一个流。</span>
<span class="nc">String</span><span class="o">[]</span> <span class="n">arrayOfWords</span> <span class="o">=</span> <span class="o">{</span><span class="s">"goodbye"</span><span class="o">,</span> <span class="s">"world"</span><span class="o">};</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">uniqueCharacters</span> <span class="o">=</span> <span class="n">words</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">w</span> <span class="o">-></span> <span class="n">w</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="s">""</span><span class="o">)).</span><span class="na">flatMap</span><span class="o">(</span><span class="nl">Arrays:</span><span class="o">:</span><span class="n">stream</span><span class="o">)</span>
<span class="o">.</span><span class="na">distinct</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collections</span><span class="o">.</span><span class="na">toList</span><span class="o">());</span>
<span class="c1">// 查找与数据匹配(anyMatch只要有任意一个元素匹配就会返回`true`).</span>
<span class="k">if</span><span class="o">(</span><span class="n">menu</span><span class="o">.</span><span class="na">Stream</span><span class="o">().</span><span class="na">anyMatch</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">isVegetable</span><span class="o">))</span> <span class="o">{</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"the vegetable is (somewhat) vegetarian friendly"</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// 检查谓词是否匹配所有的元素.</span>
<span class="kt">boolean</span> <span class="n">idHealthy</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">allMatch</span><span class="o">(</span><span class="n">d</span> <span class="o">-></span> <span class="n">d</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o"><</span><span class="mi">1000</span><span class="o">);</span>
<span class="c1">// noneMatch其中没有任何一个元素匹配.</span>
<span class="kt">boolean</span> <span class="n">isHealthy</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">noneMatch</span><span class="o">(</span><span class="n">d</span> <span class="o">-></span> <span class="n">d</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1000</span><span class="o">);</span>
<span class="c1">// 使用findAny对数据进行过滤和筛选,返回一个Optional<Dish>,如果其包含元素则打印出元素的内容.</span>
<span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">isVegetarian</span><span class="o">).</span><span class="na">findAny</span><span class="o">().</span><span class="na">isPresent</span><span class="o">(</span><span class="n">d</span> <span class="o">-></span> <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">getName</span><span class="o">()));</span>
</code></pre></div></div>
<p>有些流有一个出现顺序来指定流中项目出现的逻辑顺序(比如由<code class="language-plaintext highlighter-rouge">List</code>排序好的数据列生成的流)。对于这种流,你可能就像找到第一个元素,为此存在一个findAny的方法;到目前为止,见到过的终端操作都是返回一个<code class="language-plaintext highlighter-rouge">boolean</code>值。对于将一个流中所有的元素都结合起来的操作可以使用;</p>
<p><code class="language-plaintext highlighter-rouge">reduce</code>操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或者“菜单中卡路里最高的菜是哪一个”此类的查询。此类操作需要将所有元素反复结合起来得到一个值。这样的查询可以被归类为规约操作(将流规约为一个数值)。</p>
<p><strong>规约方法的优势与并行化</strong>,使用<code class="language-plaintext highlighter-rouge">reduce</code>的好处在于这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行reduce操作。而迭代式求和例子中要更新共享变量<code class="language-plaintext highlighter-rouge">sum</code>这并不是并行化的。如果加入同步的话,很可能会发现线程竞争抵消了并行本应带来的性能提升,这种计算是通过引入一种<code class="language-plaintext highlighter-rouge">fork/join</code>的模式对任务进行计算的;数值范围是一个常用的东西,其可以代替java中的for循环并且语法也更加的简单。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">List</span><span class="o"><</span><span class="nc">Integer</span><span class="o">></span> <span class="n">someNumbers</span> <span class="o">=</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">alList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="mi">4</span><span class="o">,</span> <span class="mi">5</span><span class="o">);</span>
<span class="nc">Option</span><span class="o"><</span><span class="nc">Integer</span><span class="o">></span> <span class="n">firstSquareDivisibleByThree</span> <span class="o">=</span> <span class="n">someNumbers</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">x</span> <span class="o">-></span> <span class="n">x</span><span class="o">*</span><span class="n">x</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">x</span> <span class="o">-></span> <span class="n">x</span><span class="o">%</span><span class="mi">3</span> <span class="o">==</span> <span class="mi">0</span><span class="o">).</span><span class="na">findFirst</span><span class="o">();</span>
<span class="c1">// 对流中所有的元素进行求和.</span>
<span class="kt">int</span> <span class="n">sum</span> <span class="o">=</span> <span class="n">numbers</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">reduce</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="o">(</span><span class="n">a</span><span class="o">,</span> <span class="n">b</span><span class="o">)</span> <span class="o">-></span> <span class="n">a</span><span class="o">+</span><span class="n">b</span><span class="o">);</span>
<span class="c1">// 对于求最大值和最小值的操作,你可以使用Integer.max以及min的方法.</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">Integer</span><span class="o">></span> <span class="n">max</span> <span class="o">=</span> <span class="n">numbers</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">reduce</span><span class="o">(</span><span class="nl">Integer:</span><span class="o">:</span><span class="n">max</span><span class="o">);</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">Integer</span><span class="o">></span> <span class="n">min</span> <span class="o">=</span> <span class="n">numbers</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">reduce</span><span class="o">(</span><span class="nl">Integer:</span><span class="o">:</span><span class="n">min</span><span class="o">);</span>
<span class="c1">// 计算0~100中有多少个偶数,首先生成一个范围.并向控制台打印出偶数元素的个数.</span>
<span class="nc">IntStream</span> <span class="n">evenNumbers</span> <span class="o">=</span> <span class="nc">IntStream</span><span class="o">.</span><span class="na">rangeClosed</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">100</span><span class="o">).</span><span class="na">filter</span><span class="o">(</span><span class="n">n</span> <span class="o">-></span> <span class="n">n</span><span class="o">%</span><span class="mi">2</span> <span class="o">==</span> <span class="mi">0</span><span class="o">);</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">evenNumbers</span><span class="o">.</span><span class="na">count</span><span class="o">());</span>
<span class="c1">// 构建java流的方式.</span>
<span class="nc">Stream</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">stream</span> <span class="o">=</span> <span class="nc">Stream</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"java 8"</span><span class="o">,</span> <span class="s">"lambda"</span><span class="o">,</span> <span class="s">"in"</span><span class="o">,</span> <span class="s">"action"</span><span class="o">);</span>
<span class="c1">// 通过数组创建流.</span>
<span class="kt">int</span><span class="o">[]</span> <span class="n">numbers</span> <span class="o">=</span> <span class="o">{</span><span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="mi">5</span><span class="o">,</span> <span class="mi">7</span><span class="o">,</span> <span class="mi">11</span><span class="o">,</span> <span class="mi">13</span><span class="o">};</span>
<span class="kt">int</span> <span class="n">sum</span> <span class="o">=</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">stream</span><span class="o">(</span><span class="n">numbers</span><span class="o">).</span><span class="na">sum</span><span class="o">();</span>
</code></pre></div></div>
<p><strong>用流收集数据</strong>:将流中的元素积累成一个汇总结果,具体的做法就是通过定义新的<code class="language-plaintext highlighter-rouge">Collector</code>接口来定义的,因此区分<code class="language-plaintext highlighter-rouge">Collection、Collector和collect</code>是很重要的。关于使用collect和收集器可以做什么:对于一个交易列表按货币进行分组,获得该货币的所有交易额总和(返回一个<code class="language-plaintext highlighter-rouge">Map<Currency, Integer></code>);将交易列表分为两组,贵的和不贵的返回一个<code class="language-plaintext highlighter-rouge">Map<Boolean, List<Transaction>></code>;可以使用<code class="language-plaintext highlighter-rouge">counting()</code>工厂方法返回收集器,统计流中元素的个数;</p>
<p>使用<code class="language-plaintext highlighter-rouge">maxBy和minBy</code>获取得到数据流中的最大值和最小值;java 8实现了大多数的规约操作,但是仍然有一些操作需要我们进行自定义,这也就是reducing规约存在的意义。在使用reducing进行统计的时候第一个参数的值代表的是规约操作的起始值,第二个参数就是你在6.2节中使用的函数,将菜肴转换成表示其所含热量的int。第三个参数是一个<code class="language-plaintext highlighter-rouge">BinaryOperator</code>将两个项目累积成一个同类型的值,这里它就是两个Int求和的结果;分组,一个常见的数据库操作就是根据一个或多个属性对集合中的项目进行分组,就像前面讲到的对货币进行分组一样。</p>
<p>在java8之前实现此类的操作略显复杂,但如果使用Java 8所推崇的函数式风格来重写的话,就很容易转化为一个非常容易看懂的语句。分区是分组的特殊情况,由一个谓词作为分类函数,它成为分区函数。分区函数返回一个布尔值,这意味着得到的分组map的键类型是<code class="language-plaintext highlighter-rouge">Boolean</code>,因而正常情况下分组只会有两种结果,true是一组false是一组结果。分区的好处在于保留了分区函数返回<code class="language-plaintext highlighter-rouge">true</code>和<code class="language-plaintext highlighter-rouge">false</code>的两套流元素列表。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1.统计菜单列表中菜品的总数.</span>
<span class="kt">long</span> <span class="n">howManyDishes</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">counting</span><span class="o">());</span>
<span class="kt">long</span> <span class="n">howManyDishes</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">count</span><span class="o">();</span>
<span class="c1">// 2.查找流中的最大值和最小值.</span>
<span class="nc">Comparator</span><span class="o"><</span><span class="nc">Dish</span><span class="o">></span> <span class="n">dishCaloriesComparator</span> <span class="o">=</span> <span class="nc">Comparator</span><span class="o">.</span><span class="na">comparing</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getCalories</span><span class="o">);</span>
<span class="c1">// 返回的Optional类是为了解决Java中的空指针问题.</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">Dish</span><span class="o">></span> <span class="n">mostCalorieDish</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="n">maxBy</span><span class="o">(</span><span class="n">dishCaloriesComparator</span><span class="o">));</span>
<span class="c1">// 3.统计数据流中所有元素总和summingInt</span>
<span class="kt">int</span> <span class="n">totalCalories</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="n">summing</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getCalories</span><span class="o">));</span>
<span class="c1">// 4.对于求数据流中元素的平均值来说,可以使用averagingInt来进行处理.</span>
<span class="kt">double</span> <span class="n">avgCalories</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="n">averagingInt</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getCalories</span><span class="o">));</span>
<span class="c1">// 5.使用joining对数据流中的每个元素调用其toString方法进行拼接.</span>
<span class="nc">String</span> <span class="n">shortMenu</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getName</span><span class="o">).</span><span class="na">collect</span><span class="o">(</span><span class="n">joining</span><span class="o">());</span>
<span class="c1">// 6.使用reducing规约进行操作.</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">Dish</span><span class="o">></span> <span class="n">mostCalorieDish</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="n">reducing</span><span class="o">(</span><span class="n">d1</span><span class="o">,</span> <span class="n">d2</span><span class="o">)</span> <span class="o">-></span> <span class="n">d1</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o">></span> <span class="n">d2</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o">?</span> <span class="n">d1</span> <span class="o">:</span> <span class="n">d2</span><span class="o">));</span>
<span class="c1">// 使用reducing规约来计算你菜单的总热量.</span>
<span class="kt">int</span> <span class="n">totalCalories</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="n">reducing</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="nl">Dish:</span><span class="o">:</span><span class="n">getCalories</span><span class="o">,</span> <span class="o">(</span><span class="n">i</span><span class="o">,</span> <span class="n">j</span><span class="o">)</span> <span class="o">-></span> <span class="n">i</span> <span class="o">+</span> <span class="n">j</span><span class="o">));</span>
<span class="c1">// 7.对数据流进行分组group by,将菜单中的菜肴按照类型进行分类.给groupingBy提供了一个传递Function,它提取了流中每一道Dish的Dish.Type,我们将这个函数叫做分类函数.</span>
<span class="nc">Map</span><span class="o"><</span><span class="nc">Dish</span><span class="o">.</span><span class="na">Type</span><span class="o">,</span><span class="nc">List</span><span class="o"><</span><span class="nc">Dish</span><span class="o">>></span> <span class="n">dishesType</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="n">groupingBy</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getType</span><span class="o">));</span>
<span class="c1">// 多级分组对于groupingBy工厂方法创建的收集器中,它除了普通的分类函数外,还可以接受collector类型的第二个参数。要么进行二级分组的话,我们可以将一个内层的groupingBy传递给外层的groupingBy.</span>
<span class="nc">Map</span><span class="o"><</span><span class="nc">Dish</span><span class="o">.</span><span class="na">Type</span><span class="o">,</span> <span class="nc">Map</span><span class="o"><</span><span class="nc">CalorieLevel</span><span class="o">,</span> <span class="nc">List</span><span class="o"><</span><span class="nc">Dish</span><span class="o">>></span> <span class="n">dishesByTypeCaloriesLevel</span> <span class="o">=</span>
<span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getType</span><span class="o">,</span>
<span class="n">groupingBy</span><span class="o">(</span><span class="n">dish</span><span class="o">->{</span>
<span class="k">if</span><span class="o">(</span><span class="n">dish</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o"><=</span> <span class="mi">400</span><span class="o">)</span> <span class="k">return</span> <span class="nc">CaloricLevel</span><span class="o">.</span><span class="na">DIET</span><span class="o">;</span>
<span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">dish</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o"><=</span> <span class="mi">700</span><span class="o">)</span> <span class="k">return</span> <span class="nc">CaloricLevel</span><span class="o">.</span><span class="na">NORMAL</span><span class="o">;</span>
<span class="k">else</span> <span class="k">return</span> <span class="nc">CaloricLevel</span><span class="o">.</span><span class="na">FAT</span><span class="o">;</span>
<span class="o">})</span>
<span class="o">)</span>
</code></pre></div></div>
<p><strong>并行处理数据集合时,你需要考虑的事情</strong>:你得明确的把包含数据的数据结构分为若干子部分。第二,你要给每个子部分分配一个独立的线程。第三,你需要在恰当的时候对它们进行同步来避免不希望出现的竞争条件。等待所有线程完成,最后把这些部分结果合并起来,java 7版本的时候引入了fork/join的多线程框架用于处理此类任务。
引入Stream流操作之后,它允许你声明性的将顺序流变为并行流。在现实中对顺序流调用parallel方法并不意味着流本身有任何实际的变化,它在内部实际上就是设置了一个<code class="language-plaintext highlighter-rouge">boolean</code>标志,表示你想让调用parallel之后进行的所有操作都并行执行。内部迭代让你可以并行的处理一个流,而无需在代码中显示使用和协调不同的线程。分支/合并框架让你得以用递归的方式将可以并行的任务拆分成更小的任务,在不同的线程上执行,然后将各个子任务的结果合并起来生成整体的结果。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 将顺序流转换成为并行流进行计算.</span>
<span class="nc">Stream</span><span class="o">.</span><span class="na">iterate</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="n">i</span> <span class="o">-></span> <span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="o">).</span><span class="na">limit</span><span class="o">(</span><span class="n">n</span><span class="o">).</span><span class="na">parallel</span><span class="o">().</span><span class="na">reduce</span><span class="o">(</span><span class="mi">0L</span><span class="o">,</span> <span class="nl">Long:</span><span class="o">:</span><span class="n">sum</span><span class="o">);</span>
</code></pre></div></div>
<p><strong>java 8提高编程的效率</strong>:相比较于匿名类,lambda表达式可以帮助我们用更紧凑的方式描述程序的行为,如果希望将一个既有的方法作为参数传递给另一个方法,那么方法引用无意是我们推荐的方法,利用这种方式我们能够写出非常简介的代码。跟之前的版本相比较,java 8的新特性也可以帮助提升代码的可读性:使用java 8你可以减少冗长的代码,代码更易于理解。通过方法引用和<code class="language-plaintext highlighter-rouge">Stream API</code>你的代码会更加直观。</p>
<p><strong>代码的重构</strong> 主要有3种简单的方式:重构代码,用<code class="language-plaintext highlighter-rouge">lambda</code>表达式代替匿名内部类,用方法引用重构lambda表达式,用<code class="language-plaintext highlighter-rouge">Stream API</code>重构命令式的数据处理。</p>
<ul>
<li>需要注意的地方,在有些情况下将匿名类转换成为lambda表达式可能是一个比较复杂的过程。匿名类中的<code class="language-plaintext highlighter-rouge">this</code>和<code class="language-plaintext highlighter-rouge">super</code>的含义与lambda中的含义不同,在lambda表达式中<code class="language-plaintext highlighter-rouge">this</code>指代的的是外部所在类,而不是匿名类中的自身。另外一点,匿名类可以屏蔽外部类中的变量名称,而lambda表达式则不能因为其会导致编译错误。</li>
<li>可以通过方法引用将lambda表达式中的内容抽取到一个单独的方法中,将其作为参数传递给<code class="language-plaintext highlighter-rouge">groupingBy</code>方法。在使用方法引用的时候还应尽量的参考<code class="language-plaintext highlighter-rouge">comparing、maxBy</code>等方法。</li>
<li>从命令式的数据处理转换到<code class="language-plaintext highlighter-rouge">Stream</code>,java 8中的流式操作能够更加清晰的表达数据处理管道的意图,除此之外,通过短路和延迟加载以及之前介绍的现代计算机的多核架构,在内部可以对Stream进行优化。
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Runnable</span> <span class="n">r1</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Runnable</span><span class="o">()</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span> <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"hello"</span><span class="o">);</span> <span class="o">}</span>
<span class="o">}</span>
<span class="c1">// 1.新的方式,使用lambda表达式代替内名内部类。</span>
<span class="nc">Runnable</span> <span class="n">r2</span> <span class="o">=</span> <span class="o">()</span> <span class="o">-></span> <span class="o">{</span> <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"hello"</span><span class="o">);</span> <span class="o">}</span>
<span class="c1">// 2.按照Dish的level等级将菜单中的菜品进行分组(使用方法引用代替lambda中的判断逻辑).</span>
<span class="nc">Map</span><span class="o"><</span><span class="nc">CaloricLevel</span><span class="o">,</span> <span class="nc">List</span><span class="o"><</span><span class="nc">Dish</span><span class="o">>></span> <span class="n">dishesByCaloricLevel</span> <span class="o">=</span> <span class="n">menu</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">collect</span><span class="o">(</span><span class="n">groupingBy</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getCaloricLevel</span><span class="o">));</span>
<span class="c1">// 3.使用现代计算机中的多核架构parallel代替命令式的数据流处理.</span>
<span class="n">menu</span><span class="o">.</span><span class="na">parallelStream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="n">d</span> <span class="o">-></span> <span class="n">d</span><span class="o">.</span><span class="na">getCalories</span><span class="o">()</span> <span class="o">></span> <span class="mi">300</span><span class="o">).</span><span class="na">map</span><span class="o">(</span><span class="nl">Dish:</span><span class="o">:</span><span class="n">getName</span><span class="o">).</span><span class="na">collect</span><span class="o">(</span><span class="n">toList</span><span class="o">());</span>
</code></pre></div> </div>
</li>
</ul>
<p>使用java8中的lambda表达式对于设计模式中的重构:对于策略设计模式我们使用lambda表达式直接传递代码避免了僵尸代码的出现,对于给定的接口使用lambda表达式进行实现。对于模板设计模式的优化也是将代码传递到了方法参数中,不再需要对基类进行继承。</p>
<h3 id="4-java8中的默认方法">4. java8中的默认方法:</h3>
<p>在java中接口将相关的方法按照约定组合到一起,实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。但是一旦类库的设计者需要更新接口向其中添加新的方法,这种方式就会出现问题。java8为了解决这个问题引入了一种新的设计机制,java8的接口现在支持在声明方法的同时提供实现。</p>
<p>可以通过两种方式进行实现:一种为java8允许在接口内声明静态方法。其二是java8中引入了一个新的功能叫做默认方法,通过默认方法可以指定接口的默认实现,也就是接口能够提供方法的默认实现。默认方法在java8中已经大量的使用了,如<code class="language-plaintext highlighter-rouge">Collection</code>类的<code class="language-plaintext highlighter-rouge">stream</code>方法就是默认方法,<code class="language-plaintext highlighter-rouge">List</code>接口的<code class="language-plaintext highlighter-rouge">sort</code>方法以及之前介绍的很多函数式接口<code class="language-plaintext highlighter-rouge">Predicate、Function以及Comparator</code>也引入了新的默认方法。</p>
<p>java8中抽象类和抽象接口之间的区别:在继承关系上一个类只能继承一个抽象类,但是一个类可以实现多个接口。其次,一个抽象类可以通过实例变量保存一个通用的状态,而接口是不能够有实例变量的。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 在jdk1.8中为List接口新增的默认方法,函数方法sort前面的修饰符default能够知道一个方法是否为默认方法.</span>
<span class="k">default</span> <span class="kt">void</span> <span class="nf">sort</span><span class="o">(</span><span class="nc">Comparator</span><span class="o"><?</span> <span class="kd">super</span> <span class="no">E</span><span class="o">></span> <span class="n">c</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Collections</span><span class="o">.</span><span class="na">sort</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">c</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>null带来的种种问题,首先<code class="language-plaintext highlighter-rouge">NullPointerException</code>是目前java程序开发中最典型的异常。此外在代码中进行着空指针的检查会使得你的代码可读性糟糕透顶。<code class="language-plaintext highlighter-rouge">null</code>自身是没有任何的语义,尤其是它代表的是在静态语言类型中以一种错误的方式对缺失变量值得建模。其它语言对于<code class="language-plaintext highlighter-rouge">null</code>的处理。</p>
<p>在Groovy语言中通过引入安全导航操作符可以安全的访问可能为<code class="language-plaintext highlighter-rouge">null</code>的变量。其语法解释为当某个属性的值为null的时候,语法分析将不会再继续往后处理。<code class="language-plaintext highlighter-rouge">person</code>可能没有<code class="language-plaintext highlighter-rouge">car</code>的属性,在调用链中如果遭遇了<code class="language-plaintext highlighter-rouge">null</code>时将<code class="language-plaintext highlighter-rouge">null</code>引用沿着调用链传递下去,返回一个<code class="language-plaintext highlighter-rouge">null</code>的值。在spring El表达式中也存在于groovy类似的语法用于对对象的属性进行索引。
<code class="language-plaintext highlighter-rouge">def carInsuranceName = person?.car?.insurance?.name;</code></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// java中的spring El表达式其也采用了与groovy类似的语法用于获取对象的属性值.</span>
<span class="nc">String</span> <span class="n">city</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="na">parseExpression</span><span class="o">(</span><span class="s">"PlaceOfBirth?.City"</span><span class="o">).</span><span class="na">getValue</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">city</span><span class="o">);</span> <span class="c1">// Smiljan</span>
</code></pre></div></div>
<p>汲取Haskell和Scale的灵感,java 8中引入了一个新的类<code class="language-plaintext highlighter-rouge">java.util.Optional<T></code>,有时候还可以通过此类判断当前jdk的版本(spring core中使用了这种方法)。这是一个封装<code class="language-plaintext highlighter-rouge">Optional</code>值得类。当变量存在的时候<code class="language-plaintext highlighter-rouge">Optional</code>类只是对类的简单封装。变量不存在的时候,缺失的值会进行自动建模成一个“空”的Optional对象,由方法<code class="language-plaintext highlighter-rouge">Option.empty()</code>返回。</p>
<p>应用<code class="language-plaintext highlighter-rouge">Optional</code>的几种模式:使用map从<code class="language-plaintext highlighter-rouge">Optional</code>对象中提取和转换值,map操作会将提供的函数应用于流中的每个元素,可以将<code class="language-plaintext highlighter-rouge">Optional</code>当做一个特殊的集合,它至多包含一个元素。如果是递归调用操作调用属性值的话,则不能使用<code class="language-plaintext highlighter-rouge">map</code>应该使用扁平化的数据流<code class="language-plaintext highlighter-rouge">flatMap</code>进行操作;</p>
<p>默认行为以及解引用<code class="language-plaintext highlighter-rouge">Optional</code>对象:get()是这些方法中最简单但又不安全的方法,如果变量存在则返回变量的值否则抛出<code class="language-plaintext highlighter-rouge">NoSuchElementException</code>的异常;orElse()操作允许你在<code class="language-plaintext highlighter-rouge">Optional</code>对象不存在的时候提供一个默认值;<code class="language-plaintext highlighter-rouge">ifPresent(Consumer<? super T>)</code>能让变量在存在的时候执行一个以参数传递进来的方法。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 声明一个空的Optional,通过其静态方法创建一个空的Optional对象.</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">Car</span><span class="o">></span> <span class="n">optCar</span> <span class="o">=</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">empty</span><span class="o">();</span>
<span class="c1">// 依据一个非空值创建Optional,我们可以使用Optional.of依据一个非空值创建一个Optional对象.如果car的值为null的话则会立即抛出NullPointerException.</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">Car</span><span class="o">></span> <span class="n">optCar</span> <span class="o">=</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">car</span><span class="o">);</span>
<span class="c1">// 可接受null的Optional,可以使用ofNullable方法创建一个允许null值得Optional对象.</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">Car</span><span class="o">></span> <span class="n">optCar</span> <span class="o">=</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">car</span><span class="o">);</span>
<span class="c1">// 通过map方法获取得到Optional对象中的属性值.</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">Insurance</span><span class="o">></span> <span class="n">optInsurance</span> <span class="o">=</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">insurance</span><span class="o">);</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">name</span> <span class="o">=</span> <span class="n">optInsurance</span><span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Insurance:</span><span class="o">:</span><span class="n">getName</span><span class="o">);</span>
<span class="c1">// 使用flatMap操作属性.属性的值.如果Optional的结果值为空设置默认值.</span>
<span class="n">person</span><span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="nl">Person:</span><span class="o">:</span><span class="n">getCar</span><span class="o">).</span><span class="na">flatMap</span><span class="o">(</span><span class="nl">Car:</span><span class="o">:</span><span class="n">getInsurance</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Insurance:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">orElse</span><span class="o">(</span><span class="s">"Unknown"</span><span class="o">);</span>
</code></pre></div></div>
<p>java8中新引入的<code class="language-plaintext highlighter-rouge">CompletableFuture</code>接口构建异步的应用,其弥补了之前<code class="language-plaintext highlighter-rouge">Future</code>接口在这些方面的不足:将两个异步计算合并为一个—这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。等待<code class="language-plaintext highlighter-rouge">Future</code>集合中的所有任务都完成。仅等待Future集合中最快结束的任务完成,并返回它们的结果。通过编程的方式完成一个<code class="language-plaintext highlighter-rouge">Future</code>任务的执行(即以手工设定异步操作结果的方式)。应对Future的完成事件(即当Future的完成事件发生时会收到通知,并使用Future计算的结果进行下一步的操作,不只是简单地阻塞等待结果)。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Future</span><span class="o"><</span><span class="nc">Double</span><span class="o">></span> <span class="nf">getPriceAsync</span><span class="o">(</span><span class="nc">String</span> <span class="n">product</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">CompletableFuture</span><span class="o"><</span><span class="nc">Double</span><span class="o">></span> <span class="n">futurePrice</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CompletableFuture</span><span class="o"><>();</span>
<span class="k">new</span> <span class="nf">Thread</span><span class="o">(()</span> <span class="o">-></span> <span class="o">{</span>
<span class="kt">double</span> <span class="n">price</span> <span class="o">=</span> <span class="n">calculatePrice</span><span class="o">(</span><span class="n">product</span><span class="o">);</span>
<span class="n">futurePrice</span><span class="o">.</span><span class="na">complete</span><span class="o">(</span><span class="n">price</span><span class="o">);</span>
<span class="o">}).</span><span class="na">start</span><span class="o">();</span>
<span class="o">}</span>
<span class="c1">// 当在客户端调用该方法的时候回字节返回future结果,等其它操作结束之后可以调用future.get()获取计算的结果.如果价格未知,</span>
<span class="c1">// 则get方法会一直处于阻塞的状态直至方法调用结束.</span>
<span class="kt">double</span> <span class="n">price</span> <span class="o">=</span> <span class="n">future</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
</code></pre></div></div>