<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; Go</title>
	<atom:link href="https://blog.gmem.cc/category/work/go/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 03 Apr 2026 04:13:36 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Casbin学习笔记</title>
		<link>https://blog.gmem.cc/casbin</link>
		<comments>https://blog.gmem.cc/casbin#comments</comments>
		<pubDate>Thu, 30 Jan 2020 04:02:24 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=33711</guid>
		<description><![CDATA[<p>简介 Casbin是一个权限控制的开发库，它的特性包括： 支持多种编程语言，包括Go、Java、Node.js、PHP、Python等 支持ACL、RBAC、ABAC等多种访问控制模型 支持以典型的[crayon-69d3148a61b36051300353-i/]形式，或者自定义的形式来定义策略，allow/deny授权均支持 支持处理访问控制模型，及其策略的存取（和存储后端交互） 支持管理角色-用户映射，以及角色-角色映射（RBAC中的角色层次） 支持内置超级用户，例如root/administrator，不需要明确授权就可以作任何事情 很多内置的操作符，来支持规则匹配 Casbin不负责： 身份验证 管理角色、用户的详细信息。但是它维护角色和用户之间的关系 访问控制模型 Casbin支持的访问控制模型列表： 模型 说明 ACL 为对象添加一个访问许可（Permissions）列表，每个条目指定主体（Subject，例如用户/进程）可以对对象执行何种操作（Action） 模型示例： [crayon-69d3148a61b3d116136169/] 策略示例： [crayon-69d3148a61b40748837529/] <a class="read-more" href="https://blog.gmem.cc/casbin">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/casbin">Casbin学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Casbin是一个权限控制的开发库，它的特性包括：</p>
<ol>
<li>支持多种编程语言，包括Go、Java、Node.js、PHP、Python等</li>
<li>支持ACL、RBAC、ABAC等多种访问控制模型</li>
<li>支持以典型的<pre class="crayon-plain-tag">{subject, object, action}</pre>形式，或者自定义的形式来定义策略，allow/deny授权均支持</li>
<li>支持处理访问控制模型，及其策略的存取（和存储后端交互）</li>
<li>支持管理角色-用户映射，以及角色-角色映射（RBAC中的角色层次）</li>
<li>支持内置超级用户，例如root/administrator，不需要明确授权就可以作任何事情</li>
<li>很多内置的操作符，来支持规则匹配</li>
</ol>
<p>Casbin不负责：</p>
<ol>
<li>身份验证</li>
<li>管理角色、用户的详细信息。但是它维护角色和用户之间的关系</li>
</ol>
<div class="blog_h2"><span class="graybg">访问控制模型</span></div>
<p>Casbin支持的访问控制模型列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">模型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ACL</td>
<td>
<p>为对象添加一个访问许可（Permissions）列表，每个条目指定主体（Subject，例如用户/进程）可以对对象执行何种操作（Action）</p>
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act</pre>
<p>策略示例：</p>
<pre class="crayon-plain-tag">p, alice, data1, read
p, bob, data2, write </pre>
</td>
</tr>
<tr>
<td>ACL with superuser</td>
<td>
<p>ACL模型，外加指定特权用户
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act || r.sub == "root"</pre>
</td>
</tr>
<tr>
<td>ACL without users</td>
<td>
<p>用于没有身份验证机制/用户登陆的系统
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = obj, act

[policy_definition]
p = obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.obj == p.obj &amp;&amp; r.act == p.act</pre>
<p>策略示例：</p>
<pre class="crayon-plain-tag">p, data1, read
p, data2, write</pre>
</td>
</tr>
<tr>
<td>ACL without resources</td>
<td>
<p>针对资源类别，而非资源实例进行访问控制。例如write-article, read-log，不去控制某个article、log的访问权限
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, act

[policy_definition]
p = sub, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub &amp;&amp; r.act == p.act</pre>
</td>
</tr>
<tr>
<td>RBAC</td>
<td>
<p>基于角色的访问控制
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
#   如果请求主体（用户）属于策略主体（角色）
m = g(r.sub, p.sub) &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act</pre>
<p>策略示例：</p>
<pre class="crayon-plain-tag">p, alice, data1, read
p, bob, data2, write
p, data2_admin, data2, read
p, data2_admin, data2, write
g, alice, data2_admin </pre>
</td>
</tr>
<tr>
<td>RBAC with resource roles</td>
<td>
<p>用户、资源都可以具有角色（roles，或groups）
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _
g2 = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) &amp;&amp; g2(r.obj, p.obj) &amp;&amp; r.act == p.act</pre>
<p>策略示例：</p>
<pre class="crayon-plain-tag">p, alice, data1, read
p, bob, data2, write
p, data_group_admin, data_group, write

g, alice, data_group_admin
g2, data1, data_group
g2, data2, data_group</pre>
</td>
</tr>
<tr>
<td>RBAC with domains/tenants</td>
<td>
<p>对于不同的域（domain）/租户（tenant），用户可以具有不同的角色
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub, r.dom) &amp;&amp; r.dom == p.dom &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act</pre>
<p>策略示例：</p>
<pre class="crayon-plain-tag">p, admin, domain1, data1, read
p, admin, domain1, data1, write
p, admin, domain2, data2, read
p, admin, domain2, data2, write
g, alice, admin, domain1
g, bob, admin, domain2 </pre>
</td>
</tr>
<tr>
<td>ABAC</td>
<td>
<p>基于属性的角色控制，也叫（Policy-Based Access Control）或CBAC（Claims-Based Access Control）
<p>不同于常见的将用户通过某种方式关联到权限的方式，ABAC通过动态计算<span style="background-color: #c0c0c0;">一个或一组属性是否满足某种条件</span>来进行授权判断（可以编写简单的逻辑）。属性通常来说分为四类：</p>
<ol>
<li>用户属性（如用户年龄）</li>
<li>环境属性（如当前时间）</li>
<li>操作属性（如读取）</li>
<li>对象属性（如一篇文章，又称资源属性）</li>
</ol>
<p>理论上能够实现非常灵活的权限控制，几乎能满足所有类型的需求</p>
<p>举例来说，规则：允许所有班主任在上课时间自由进出校门，班主任是用户的角色熟悉，上课时间是环境属性，进出是操作属性，校门则是对象属性</p>
<p>ABAC的缺点是过于复杂，因此K8S在1.8版本引入RBAC</p>
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == r.obj.Owner</pre>
</td>
</tr>
<tr>
<td>RESTful</td>
<td>
<p>支持以HTTP动词，以及/res/*, /res/:id这样的路径来描述操作和资源 
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
#                     通配符
m = r.sub == p.sub &amp;&amp; keyMatch(r.obj, p.obj) &amp;&amp; regexMatch(r.act, p.act)</pre>
<p>策略示例：</p>
<pre class="crayon-plain-tag">p, alice, /alice_data/*, GET
p, alice, /alice_data/resource1, POST

p, bob, /alice_data/resource2, GET
p, bob, /bob_data/*, POST

p, cathy, /cathy_data, (GET)|(POST)</pre>
</td>
</tr>
<tr>
<td>Deny-override </td>
<td>
<p>支持allow、deny，deny可以覆盖allow
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act, eft

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow)) &amp;&amp; !some(where (p.eft == deny))

[matchers]
m = g(r.sub, p.sub) &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act</pre>
<p>策略示例：</p>
<pre class="crayon-plain-tag">p, alice, data1, read, allow
p, bob, data2, write, allow
p, data2_admin, data2, read, allow
p, data2_admin, data2, write, allow
p, alice, data2, write, deny

g, alice, data2_admin</pre>
</td>
</tr>
<tr>
<td>Priority</td>
<td>
<p>策略规则可以支持优先级
<p>模型示例：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act, eft

[role_definition]
g = _, _

[policy_effect]
e = priority(p.eft) || deny

[matchers]
m = g(r.sub, p.sub) &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act</pre>
<p>策略示例：</p>
<pre class="crayon-plain-tag">p, alice, data1, read, allow
p, data1_deny_group, data1, read, deny
p, data1_deny_group, data1, write, deny
p, alice, data1, write, allow

g, alice, data1_deny_group

p, data2_allow_group, data2, read, allow
p, bob, data2, read, deny
p, bob, data2, write, deny

g, bob, data2_allow_group</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">PERM元模型</span></div>
<p>在线编辑器：
<p>在Casbin中，访问控制模型被抽象到基于PERM元模型的CONF文件中。</p>
<p>PERM元模型包含四大要素： <a href="https://casbin.org/editor/">https://casbin.org/editor/</a> 可以用于编写模型、策略。</p>
<ol>
<li>Request：关于访问请求的信息，简单的请求是主体、操作、资源构成的元组：<pre class="crayon-plain-tag">r = {sub, action, resource}</pre></li>
<li>Policy：构成一个访问规则，例如管理员可以读取用户信息，同样由三元组构成：<pre class="crayon-plain-tag">p = {sub, action, resource}</pre></li>
<li>Matchers：描述请求和策略如何匹配。最简单的方式是相等判断：<pre class="crayon-plain-tag">m = r.sub == p.sub &amp;&amp; r.action == p.action &amp;&amp; r.resource == p.resource</pre> 。对于一个请求来说，所有使用Matcher的Policy都会产生一个值，记为<pre class="crayon-plain-tag">p.eft</pre></li>
<li>Effect：联合/化简（reducing）匹配某个请求的策略，并得到最终的结果（允许或拒绝访问），例如：<pre class="crayon-plain-tag">e = some(p.eft == allow)</pre></li>
</ol>
<p>下图展示了基于PERM的模型对一个请求进行授权的过程：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/06/perm.png"><img class="alignnone  wp-image-33731" src="https://cdn.gmem.cc/wp-content/uploads/2020/06/perm.png" alt="perm" width="1021" height="419" /></a> 下面的文件是本节示例的完整模型定义（基于ACL）：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act</pre>
<p>对于该模型定义，定义授权规则的策略文件（模型是定义了公式，策略则是将具体的主体、操作、资源带入公式计算）可以如下： </p>
<pre class="crayon-plain-tag">p, alice, data1, read
p, bob, data2, write</pre>
<p>如果我们想允许admin执行任何操作，需要修改matcher： </p>
<pre class="crayon-plain-tag">[matchers]
m = r.sub == admin || (r.sub == p.sub &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act)
# 如果太长，可以  \ 结尾换行</pre>
<p>要将上述模型从ACL改为ABAC风格，我们可以在matcher中访问资源的属性： </p>
<pre class="crayon-plain-tag">[matchers]
m = r.obj.owner == r.sub &amp;&amp; (r.sub == p.sub &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act)
    # 如果对象所有者是请求主体

m = r.obj == p.obj &amp;&amp; r.act == p.act || r.obj in ('data2', 'data3')
# Golang版本的Casbin还支持 in 操作符</pre>
<p>可以看到，PERM元模型足够灵活，可以让我们以简单模型开始，并按需切换到复杂的授权模型。</p>
<div class="blog_h1"><span class="graybg">编程</span></div>
<div class="blog_h2"><span class="graybg">起步</span></div>
<p>首先，从模型文件、策略文件创建Enforcer：</p>
<pre class="crayon-plain-tag">e, _ := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")</pre>
<p>判断一个Request是否可以被允许： </p>
<pre class="crayon-plain-tag">// 请求信息
sub := "alice"
obj := "data1"
act := "read"

if res := e.Enforce(sub, obj, act); res {
    // 允许访问
} else {
    // 拒绝访问
}</pre>
<div class="blog_h2"><span class="graybg">策略管理</span></div>
<p>Casbin提供了两套管理策略（Permission）的API：</p>
<ol>
<li>Management API：底层API，完全支持Casbin策略管理</li>
<li>RBAC API：更加友好的，编写RBAC模型的API，是Managlement API的子集</li>
</ol>
<div class="blog_h2"><span class="graybg">Management API</span></div>
<div class="blog_h3"><span class="graybg">Subject管理</span></div>
<pre class="crayon-plain-tag">// 获取所有主体
allSubjects := e.GetAllSubjects()

// 获取指定的命名的策略中的所有主体
allNamedSubjects := e.GetAllNamedSubjects("p")</pre>
<div class="blog_h3"><span class="graybg">Object管理</span></div>
<pre class="crayon-plain-tag">// 获取所有资源
allObjects := e.GetAllObjects()
// 获取指定的命名的策略中所有的资源
allNamedObjects := e.GetAllNamedObjects("p")</pre>
<div class="blog_h3"><span class="graybg">Action管理</span></div>
<pre class="crayon-plain-tag">// 获取所有操作
allActions := e.GetAllActions()
allNamedActions := e.GetAllNamedActions("p")</pre>
<div class="blog_h3"><span class="graybg">Role管理</span></div>
<pre class="crayon-plain-tag">// 获取所有角色
allRoles = e.GetAllRoles()
allNamedRoles := e.GetAllNamedRoles("g")</pre>
<div class="blog_h3"><span class="graybg">Policy管理</span> </div>
<pre class="crayon-plain-tag">// 获取所有策略（授权规则）
policy = e.GetPolicy()

// 获取过滤后的策略，可以根据字段进行过滤   字段索引 字段值
filteredPolicy := e.GetFilteredPolicy(0, "alice")

// 获取命名的策略中的所有授权规则
namedPolicy := e.GetNamedPolicy("p")

filteredNamedPolicy = e.GetFilteredNamedPolicy("p", 0, "bob")


// 获取所有角色继承规则
groupingPolicy := e.GetGroupingPolicy()
filteredGroupingPolicy := e.GetFilteredGroupingPolicy(0, "alice")
namedGroupingPolicy := e.GetNamedGroupingPolicy("g")
namedGroupingPolicy := e.GetFilteredNamedGroupingPolicy("g", 0, "alice")


// 判断指定的授权规则是否存在
hasPolicy := e.HasPolicy("data2_admin", "data2", "read")
hasNamedPolicy := e.HasNamedPolicy("p", "data2_admin", "data2", "read")

// 添加一个授权规则
added := e.AddPolicy("eve", "data3", "read")

// 添加若干
rules := [][] string {
                []string {"jack", "data4", "read"},
                []string {"katy", "data4", "write"},
                []string {"leyo", "data4", "read"},
                []string {"ham", "data4", "write"},
        }

areRulesAdded := e.AddPolicies(rules)

// 添加到命名策略中
added := e.AddNamedPolicy("p", "eve", "data3", "read")
areRulesAdded := e.AddNamedPolicies("p", rules)

// 删除一个授权规则
removed := e.RemovePolicy("alice", "data1", "read")

// 删除多个授权规则
areRulesRemoved := e.RemovePolicies(rules)
//                            字段索引  字段值...
removed := e.RemoveFilteredPolicy(0, "alice", "data1", "read")
removed := e.RemoveNamedPolicy("p", "alice", "data1", "read")
areRulesRemoved := e.RemoveNamedPolicies("p", rules)
removed := e.RemoveFilteredNamedPolicy("p", 0, "alice", "data1", "read")


// 添加一个角色继承规则，如果规则已经存在，返回false
added := e.AddGroupingPolicy("group1", "data2_admin")

areRulesAdded := e.AddGroupingPolicies(rules)
added := e.AddNamedGroupingPolicy("g", "group1", "data2_admin")
areRulesAdded := e.AddNamedGroupingPolicies("g", rules)


// 删除角色继承规则
removed := e.RemoveGroupingPolicy("alice", "data2_admin")</pre>
<div class="blog_h3"><span class="graybg">Function管理</span></div>
<p>你添加一个Go函数，并在编写规则时，使用该函数进行Match： </p>
<pre class="crayon-plain-tag">func CustomFunction(key1 string, key2 string) bool {
    if key1 == "/alice_data2/myid/using/res_id" &amp;&amp; key2 == "/alice_data/:resource" {
        return true
    } else if key1 == "/alice_data2/myid/using/res_id" &amp;&amp; key2 == "/alice_data2/:id/using/:resId" {
        return true
    } else {
        return false
    }
}

func CustomFunctionWrapper(args ...interface{}) (interface{}, error) {
    key1 := args[0].(string)
    key2 := args[1].(string)

    return bool(CustomFunction(key1, key2)), nil
}

e.AddFunction("keyMatchCustom", CustomFunctionWrapper)</pre>
<div class="blog_h2"><span class="graybg">RBAC API</span></div>
<div class="blog_h3"><span class="graybg">Role管理</span></div>
<pre class="crayon-plain-tag">// 获取具有角色的用户
res := e.GetRolesForUser("alice")

// 判断用户是否具有角色
res := e.HasRoleForUser("alice", "data1_admin")

// 为用户添加角色
e.AddRoleForUser("alice", "data2_admin")

// 为用户删除角色
e.DeleteRoleForUser("alice", "data1_admin")
// 删除用户的所有角色
e.DeleteRolesForUser("alice")

// 删除一个角色
e.DeleteRole("data2_admin")



// 获取用户具有的隐式特权，不同于GetRolesForUser，该函数会同时
// 返回所有间接得到的角色（因为角色之间可以有继承关系）
// 
// 例如：
//   g, alice, role:admin             alice属于admin
//   g, role:admin, role:user         admin属于user
// 该方法会返回 ["role:admin", "role:user"]
e.GetImplicitRolesForUser("alice")


// 获取用户的特权，不同于GetPermissionsForUser，该函数会同时
// 返回来自用户所属角色的特权
e.GetImplicitPermissionsForUser("alice")</pre>
<div class="blog_h3"><span class="graybg">Permission管理</span></div>
<pre class="crayon-plain-tag">// 删除一个特权（操作）
e.DeletePermission("read")

// 为用户或角色添加一个特权
e.AddPermissionForUser("bob", "read")

// 为用户删除一个特权
e.DeletePermissionForUser("bob", "read")

// 删除用户的全部特权
e.DeletePermissionsForUser("bob")


// 查询用户的特权
e.GetPermissionsForUser("bob")


// 查看用户是否具有指定的特权
e.HasPermissionForUser("alice", []string{"read"})</pre>
<div class="blog_h2"><span class="graybg">策略持久化</span></div>
<p>在Casbin中，策略存储以适配器的形式实现。你可以使用适配器的<pre class="crayon-plain-tag">LoadPolicy()</pre>来加载策略规则，使用<pre class="crayon-plain-tag">SavePolicy()</pre>来保存规则。</p>
<p>所有可用的适配器：<a href="https://casbin.org/docs/en/adapters#supported-adapters">https://casbin.org/docs/en/adapters#supported-adapters</a></p>
<div class="blog_h3"><span class="graybg">接口</span></div>
<pre class="crayon-plain-tag">type Adapter interface {
	// 加载所有策略到model中
	LoadPolicy(model model.Model) error
	// 保存所有策略
	SavePolicy(model model.Model) error

	// 添加一个策略到后端存储，作为自动保存特性的一部分
	AddPolicy(sec string, ptype string, rule []string) error
	// 从后端存储删除一个策略，作为自动保存特性的一部分
	RemovePolicy(sec string, ptype string, rule []string) error
	// 从后端存储删除匹配的策略规则，作为自动保存特性的一部分
	RemovFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error
}


// 建模访问控制模型
type Model map[string]AssertionMap

type AssertionMap map[string]*Assertion

type Assertion struct {
	Key    string
	Value  string
	Tokens []string
	Policy [][]string
	RM     rbac.RoleManager
}

var sectionNameMap = map[string]string{
	"r": "request_definition",
	"p": "policy_definition",
	"g": "role_definition",
	"e": "policy_effect",
	"m": "matchers",
}</pre>
<div class="blog_h3"><span class="graybg">使用适配器</span></div>
<pre class="crayon-plain-tag">import (
    "github.com/casbin/casbin"
    "github.com/casbin/casbin/file-adapter"
)

a := fileadapter.NewAdapter("examples/basic_policy.csv")
// 从适配器，而非文件加载策略
e := casbin.NewEnforcer("examples/basic_model.conf", a)</pre>
<div class="blog_h3"><span class="graybg">Etcd适配器</span></div>
<p>Github地址：<a href="https://github.com/sebastianliu/etcd-adapter">https://github.com/sebastianliu/etcd-adapter</a></p>
<pre class="crayon-plain-tag">import (
	"github.com/sebastianliu/etcd-adapter"
	"github.com/casbin/casbin"
)

a := etcdadapter.NewAdapter([]string{"http://127.0.0.1:2379"}, "casbin_policy_test")

e := casbin.NewEnforcer("rbac_model.conf", a)
e.LoadPolicy()</pre>
<div class="blog_h2"><span class="graybg">角色管理器</span></div>
<p>角色管理器用于管理RBAC角色层次（用户-角色、角色-角色映射）。角色管理器可以<span style="background-color: #c0c0c0;">从Casbin策略文件，或者第三方数据源提取角色数据</span>。除了默认角色管理器，都是out-of-tree的。 </p>
<div class="blog_h3"><span class="graybg">接口</span></div>
<pre class="crayon-plain-tag">package rbac

type RoleManager interface {
	// 清空所有数据，重置到初始状态
	Clear() error
	// 在两个角色之间添加继承关系。domain是角色的前缀
	AddLink(name1 string, name2 string, domain ...string) error
	// 解除两个角色之间的继承关系
	DeleteLink(name1 string, name2 string, domain ...string) error
	// 判断两个角色之间是否有继承关系
	HasLink(name1 string, name2 string, domain ...string) (bool, error)
	// 获取用户继承的角色列表
	GetRoles(name string, domain ...string) ([]string, error)
	// 获取继承某角色的用户列表
	GetUsers(name string, domain ...string) ([]string, error)
	PrintRoles() error
}</pre>
<div class="blog_h1"><span class="graybg">实践</span></div>
<div class="blog_h2"><span class="graybg">TKE</span></div>
<p>腾讯的TKE项目使用Casbin实现了权限控制。相关代码位于auth-api和auth-controller中。</p>
<div class="blog_h3"><span class="graybg">加载策略模型</span></div>
<p>可以先将字符串解析为Model对象，然后再创建Enforcer：</p>
<pre class="crayon-plain-tag">m, err := model.NewModelFromString(authapi.DefaultRuleModel)
// SyncedEnforcer保持和文件或数据库同步
enforcer, err = casbin.NewSyncedEnforcer(m)</pre>
<div class="blog_h3"><span class="graybg">调试日志</span></div>
<pre class="crayon-plain-tag">import casbinlog "github.com/casbin/casbin/v2/log"

casbinlog.SetLogger(&amp;casbinlogger.WrapLogger{})
enforcer.EnableLog(true) </pre>
<div class="blog_h3"><span class="graybg">策略持久化适配器</span></div>
<p>在创建auth-controller的ControllerContext时，会初始化适配器：</p>
<pre class="crayon-plain-tag">adpt := util2.NewAdapter(client.AuthV1().Rules(), sharedInformers.Auth().V1().Rules().Lister())

func NewAdapter(ruleClient authv1client.RuleInterface, ruleLister authv1lister.RuleLister) *RestAdapter {
	adapter := &amp;RestAdapter{
		ruleClient: ruleClient,
		lister:     ruleLister,
	}

	return adapter
}</pre>
<p>可以猜测到，此适配器使用API Server的Lister接口，从Etcd中读取策略信息。</p>
<p>该适配器的实现：</p>
<pre class="crayon-plain-tag">type RestAdapter struct {
	ruleClient authv1client.RuleInterface
	lister     authv1lister.RuleLister
}


func (a *RestAdapter) LoadPolicy(model model.Model) error {
	// 加载所有Rule资源
	rules, err := a.lister.List(labels.Everything())
	if err != nil {
		return fmt.Errorf("list all rules failed: %v", err)
	}

	for _, rule := range rules {
		a.loadPolicy(rule, model)
	}

	return nil
}

// 将自定义资源转换为文本，然后从文本解析出策略
// V0 - V6是策略规则可能的参数，一般没有这么多参数
func (a *RestAdapter) loadPolicy(rule *authv1.Rule, model model.Model) {
	casRule := rule.Spec
	lineText := casRule.PType
	if casRule.V0 != "" {
		lineText += ", " + casRule.V0
	}
	if casRule.V1 != "" {
		lineText += ", " + casRule.V1
	}
	if casRule.V2 != "" {
		lineText += ", " + casRule.V2
	}
	if casRule.V3 != "" {
		lineText += ", " + casRule.V3
	}
	if casRule.V4 != "" {
		lineText += ", " + casRule.V4
	}
	if casRule.V5 != "" {
		lineText += ", " + casRule.V5
	}
	if casRule.V6 != "" {
		lineText += ", " + casRule.V6
	}

	persist.LoadPolicyLine(lineText, model)
}

// 将内存中的策略保存到Etcd，保存为若干Rule资源
func (a *RestAdapter) SavePolicy(model model.Model) error {
	// 删除老数据
	err := a.destroy(context.Background())
	if err != nil {
		return err
	}

	var rules []authv1.Rule

	for ptype, ast := range model["p"] {
		for _, line := range ast.Policy {
			rules = append(rules, ConvertRule(ptype, line))
		}
	}

	for ptype, ast := range model["g"] {
		for _, line := range ast.Policy {
			rules = append(rules, ConvertRule(ptype, line))
		}
	}

	return a.savePolicy(context.Background(), rules)
}

func (a *RestAdapter) savePolicy(ctx context.Context, rules []authv1.Rule) error {
	for _, rule := range rules {
		if _, err := a.ruleClient.Create(ctx, &amp;rule, metav1.CreateOptions{}); err != nil &amp;&amp; !apierrors.IsAlreadyExists(err) {
			return err
		}
	}
	return nil
}</pre>
<div class="blog_h3"><span class="graybg">定制函数</span></div>
<p>TKE使用的默认模型是：</p>
<pre class="crayon-plain-tag">[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act, eft

[role_definition]
# 请求主体, 策略主体, 租户
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow)) &amp;&amp; !some(where (p.eft == deny))

[matchers]
m = g(r.sub, p.sub, r.dom) &amp;&amp; keyMatchCustom(r.obj, p.obj) &amp;&amp; keyMatchCustom(r.act, p.act)</pre>
<p>其中keyMatchCustom是自定义的函数：</p>
<pre class="crayon-plain-tag">enforcer.AddFunction("keyMatchCustom", CustomFunctionWrapper)

func CustomFunctionWrapper(args ...interface{}) (interface{}, error) {
	key1 := args[0].(string)
	key2 := args[1].(string)

	return keyMatchCustomFunction(key1, key2), nil
}

// 目标匹配规则
//   /project:123/cluster:456 匹配 /project:*/cluster:456
//   registry:123/* 匹配 registry:123/456
func keyMatchCustomFunction(key1 string, key2 string) bool {
	key1 = strings.ToLower(key1)
	key2 = strings.ToLower(key2)

	key2 = strings.Replace(key2, "*", ".*", -1)

	re := regexp.MustCompile(`(.*):[^/]+(.*)`)
	i := 2
	for {
		if !strings.Contains(key2, "/:") {
			break
		}

		key2 = re.ReplaceAllString(key2, "$1[^/]+$2")
		i = i + 1
	}

	return casbinutil.RegexMatch(key1, "^"+key2+"$")
}</pre>
<div class="blog_h3"><span class="graybg">角色管理器</span></div>
<p>TKE使用了自定义的角色管理器：</p>
<pre class="crayon-plain-tag">rm := domainrolemanager.NewRoleManager(10)
enforcer.SetRoleManager(rm)
enforcer.StartAutoLoadPolicy(cfg.CasbinReloadInterval)</pre>
<p>该角色管理器来自github.com/dovics/domain-role-manager</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/casbin">Casbin学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/casbin/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于本地gRPC的Go插件系统</title>
		<link>https://blog.gmem.cc/go-plugin-over-grpc</link>
		<comments>https://blog.gmem.cc/go-plugin-over-grpc#comments</comments>
		<pubDate>Thu, 16 Jan 2020 05:54:03 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[RPC]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=31305</guid>
		<description><![CDATA[<p>Go插件化 Go语言缺乏好用的动态加载代码的机制，Go程序通常是单个自包含的二进制文件，因此难以实现类似于Java那样的插件系统。 两种方式 编译期插件 这种插件直接编译到二进制文件中。典型的例子是database/sql包中的数据库驱动。这种插件都是通过空白导入： [crayon-69d3148a629f6542320194/] 的方式激活，插件包的init方法负责插件的注册和初始化。这类插件的缺点包括： 违反开闭原则，引入插件必须修改依赖它的程序 运行时插件 对于编译型（直接编译为本地代码，不是JVM那样的虚拟机的代码）语言，都可以调用共享库。Go语言也不例外，其内置的plugin包，就是基于共享库实现插件化。plugin包有一些致命的缺点： 限制条件非常苛刻，对于主程序、插件共同使用的包，有任何修改，你都必须重新编译主程序和插件。否则会得到报错：plugin was built with a different version of package 主程序和插件的编译器、编译标记也必须一致 对操作系统的支持不完善，不支持Windows   <a class="read-more" href="https://blog.gmem.cc/go-plugin-over-grpc">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-plugin-over-grpc">基于本地gRPC的Go插件系统</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">Go插件化</span></div>
<p>Go语言缺乏好用的动态加载代码的机制，Go程序通常是单个自包含的二进制文件，因此难以实现类似于Java那样的插件系统。</p>
<div class="blog_h2"><span class="graybg">两种方式</span></div>
<div class="blog_h3"><span class="graybg">编译期插件</span></div>
<p>这种插件直接编译到二进制文件中。典型的例子是database/sql包中的数据库驱动。这种插件都是通过空白导入：</p>
<pre class="crayon-plain-tag">package main
import _ "name"</pre>
<p>的方式激活，插件包的init方法负责插件的注册和初始化。这类插件的缺点包括：</p>
<ol>
<li>违反开闭原则，引入插件必须修改依赖它的程序</li>
</ol>
<div class="blog_h3"><span class="graybg">运行时插件</span></div>
<p>对于编译型（直接编译为本地代码，不是JVM那样的虚拟机的代码）语言，都可以调用共享库。Go语言也不例外，其内置的plugin包，就是基于共享库实现插件化。plugin包有一些致命的缺点：</p>
<ol>
<li>限制条件非常苛刻，对于主程序、插件共同使用的包，有任何修改，你都必须重新编译主程序和插件。否则会得到报错：plugin was built with a different version of package</li>
<li>主程序和插件的编译器、编译标记也必须一致</li>
<li>对操作系统的支持不完善，不支持Windows  </li>
</ol>
<div class="blog_h1"><span class="graybg">go-plugin简介</span></div>
<p><a href="https://github.com/hashicorp/go-plugin"> hashicorp / go-plugin</a>是一个通过RPC实现的Go插件系统，在Packer、Terraform, Nomad、Vault等由HashiCorp主导的项目中均由应用。go-plugin允许应用程序通过<span style="background-color: #c0c0c0;">本地网络（本机）的gRPC调用插件</span>，规避了Go无法动态加载代码的缺点。</p>
<p>go-plugin插件由宿主进程调用，宿主进程会以插件二进制文件为映像创建子进程，并且通过单个网络连接与之通信。网络协议可以是：</p>
<ol>
<li>net/rpc：这种情况下go-plugin使用<a href="https://github.com/hashicorp/yamux">yamux</a>库对连接进行多路复用</li>
<li>gRPC：这种情况下基于HTTP2协议进行多路复用</li>
</ol>
<div class="blog_h2"><span class="graybg">特性</span></div>
<p>go-plugin的特性包括：</p>
<ol>
<li>插件是Go接口的实现：这让插件的编写、使用非常自然。对于插件的作者来说，他只需要实现一个Go接口即可；对于插件的用户来说，他只需要调用一个Go接口即可。go-plugin会处理好本地调用转换为gRPC调用的所有细节</li>
<li>跨语言支持：插件可以基于任何主流语言编写，同样可以被任何主流语言消费</li>
<li>支持复杂的参数、返回值：go-plugin可以处理接口、io.Reader/Writer等复杂类型</li>
<li>双向通信：为了支持复杂参数，宿主进程能够将接口实现发送给插件，插件也能够回调到宿主进程</li>
<li>内置日志系统：任何使用log标准库的的插件，都会将日志信息传回宿主机进程。宿主进程会在这些日志前面加上插件二进制文件的路径，并且打印日志</li>
<li>协议版本化：支持一个简单的协议版本化，增加版本号后可以基于老版本协议的插件无效化。当接口签名变化时应当增加版本</li>
<li>标准输出/错误同步：插件以子进程的方式运行，这些子进程可以自由的使用标准输出/错误，并且打印的内容会被自动同步到宿主进程，宿主进程可以为同步的日志指定一个io.Writer</li>
<li>TTY Preservation：插件子进程可以链接到宿主进程的stdin文件描述符，以便要求TTY的软件能正常工作</li>
<li>宿主进程升级：宿主进程升级的时候，插件子进程可以继续允许，并在升级后自动关联到新的宿主进程</li>
<li>加密通信：gRPC信道可以加密</li>
<li>完整性校验：支持对插件的二进制文件进行Checksum</li>
<li>插件崩溃了，不会导致宿主进程崩溃</li>
<li>容易安装：只需要将插件放到某个宿主进程能够访问的目录即可</li>
</ol>
<div class="blog_h1"><span class="graybg">架构</span></div>
<div class="blog_h2"><span class="graybg">插件接口</span></div>
<p>插件接口是宿主进程、插件进程进行通信的桥梁：</p>
<ol>
<li>宿主进程会将插件接口的实现放到自己的插件集中</li>
<li>插件进程会将插件接口的实现放到自己的插件集中，并为每个插件指定业务接口的实现。<span style="background-color: #c0c0c0;">一个插件可以实现多个业务接口</span></li>
<li>宿主进程可以：
<ol>
<li>主动创建插件进程，从而得到插件进程的监听套接字</li>
<li>关联到既有插件进程，需要手工提供插件进程的监听套接字</li>
</ol>
</li>
<li>宿主进程会调用Client方法，获得插件客户端</li>
<li>插件进程会调用Server方法，创建插件服务器端，并且将请求委托给<span style="background-color: #c0c0c0;">业务接口</span>的实现</li>
<li>宿主进程通过plugin.Client，可以获得业务接口的Stub，调用业务接口的方法会自动转换为RPC远程调用</li>
<li>插件进程上的UDS套接字监听到调用后，会解析RPC请求，并<span style="background-color: #c0c0c0;">（Lazy的）分发（Dispense）</span>给对应的业务接口Impl</li>
</ol>
<p>架构示意如下：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/01/go-plugin.png"><img class="aligncenter  wp-image-31325" src="https://cdn.gmem.cc/wp-content/uploads/2020/01/go-plugin.png" alt="go-plugin" width="701" height="272" /></a></p>
<div class="blog_h3"><span class="graybg">Plugin</span></div>
<p>接口<pre class="crayon-plain-tag">Plugin</pre>用于获得一个插件的服务器、客户端：</p>
<pre class="crayon-plain-tag">type Plugin interface {
	// 返回一个RPC服务器兼容的结构，提供客户端可以通过net/rpc调用的方法
	Server(*MuxBroker) (interface{}, error)

	// 返回一个RPC客户端
	Client(*MuxBroker, *rpc.Client) (interface{}, error)
}</pre>
<div class="blog_h3"><span class="graybg">GRPCPlugin</span></div>
<p>这个接口类似，只是通信方式基于gRPC：</p>
<pre class="crayon-plain-tag">type GRPCPlugin interface {
	// 由于gRPC插件以单例方式服务，因此该方法仅调用一次
	GRPCServer(*GRPCBroker, *grpc.Server) error

	// 插件进程退出时，context会被go-plugin关闭
	GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
}</pre>
<div class="blog_h2"><span class="graybg">plugin.Client</span></div>
<p>这个结构负责管理一个插件应用（进程）的完整生命周期，包括<span style="background-color: #c0c0c0;">创建插件进程、连接到插件进程、Dispense业务接口的实现、最后杀死进程</span>。</p>
<p>对于每个插件（二进制文件），宿主进程需要创建一个plugin.Client实例。</p>
<p>该结构的字段如下：</p>
<pre class="crayon-plain-tag">type Client struct {
	// 插件客户端配置
	config            *ClientConfig
	// 插件进程是否已经退出
	exited            bool
	l                 sync.Mutex
	// 插件进程的RPC监听地址
	address           net.Addr
	// 插件进程对象
	process           *os.Process
	// 协议客户端，宿主进程需要调用其Dispense方法来获得业务接口的Stub
	client            ClientProtocol
	// 通信协议
	protocol          Protocol
	logger            hclog.Logger
	doneCtx           context.Context
	ctxCancel         context.CancelFunc
	negotiatedVersion int

	// 用于管理 插件管理协程的生命周期
	clientWaitGroup sync.WaitGroup

	stderrWaitGroup sync.WaitGroup

	//  测试用，标记进程是否被强杀
	processKilled bool
}</pre>
<div class="blog_h3"><span class="graybg">ClientConfig</span></div>
<p>包含初始化一个插件客户端所需的配置信息，一旦初始化客户端，即不可修改：</p>
<pre class="crayon-plain-tag">type ClientConfig struct {
	// 握手信息，用于宿主、插件的匹配。如果不匹配，插件会拒绝连接
	HandshakeConfig

	// 可以调用的插件列表
	Plugins PluginSet

	// 版本化的插件列表，用于支持在客户端、服务器之间协商兼容版本
	VersionedPlugins map[int]PluginSet

	// 启动插件进程使用的命令行，不能和Reattach联用
	Cmd      *exec.Cmd
	// 连接到既有插件进程的必要信息，不能和Cmd联用
	Reattach *ReattachConfig

	// 用于在启动插件时校验二进制文件的完整性
	SecureConfig *SecureConfig

	// 基于TLS进行RPC通信时需要的信息
	TLSConfig *tls.Config

	// 客户端是否应该被plugin包自动管理
	// 如果为true，则调用CleanupClients自动清理
	// 否则用户需要负责杀掉插件客户端，默认false
	Managed bool

	// 和子进程通信使用的端口范围
	MinPort, MaxPort uint

	// 启动插件的超时
	StartTimeout time.Duration

	Stderr io.Writer
	SyncStdout io.Writer
	SyncStderr io.Writer

	// 支持的协议，如果不指定仅仅支持netrpc
	AllowedProtocols []Protocol

	// 不指定则为hclog的默认logger
	Logger hclog.Logger

	AutoMTLS bool
}</pre>
<div class="blog_h2"><span class="graybg">plugin.Serve</span></div>
<p>插件主函数的结尾，必须调用此函数来启动监听，需要传入一个ServeConfig。</p>
<div class="blog_h3"><span class="graybg">ServeConfig</span></div>
<pre class="crayon-plain-tag">type ServeConfig struct {
	// 和客户端匹配的握手配置
	HandshakeConfig

	// 调用此函数得到tls.Config
	TLSProvider func() (*tls.Config, error)

	// 插件集
	Plugins PluginSet

	// 版本化的插件集
	VersionedPlugins map[int]PluginSet

	// 如果通过gRPC提供服务，则此字段不能为空
	// 调用此函数创建一个gRPC服务器对象
	GRPCServer func([]grpc.ServerOption) *grpc.Server

	Logger hclog.Logger
} </pre>
<div class="blog_h2"><span class="graybg">HandshakeConfig</span></div>
<p>ClientConfig、ServerConfig都会嵌入此结构，此结构用于宿主、插件建立连接前的握手。</p>
<p>宿主进程创建插件进程时，会将此信息通过环境变量传递。这样插件进程能够正常启动。如果没有这些环境变量，则插件进程会退出，并提示你插件必须由宿主进程启动。</p>
<pre class="crayon-plain-tag">type HandshakeConfig struct {
	// 协议版本号
	ProtocolVersion uint

	// 用于简单的校验插件进程是否人为手工启动的
	MagicCookieKey   string
	MagicCookieValue string
}</pre>
<div class="blog_h1"><span class="graybg">示例</span></div>
<div class="blog_h2"><span class="graybg">netrpc</span></div>
<p>基于net/rpc的通信方式目前仅仅用于向后兼容，任何新开发的代码都应该考虑使用gRPC方式。</p>
<div class="blog_h3"><span class="graybg">业务接口</span></div>
<p>很简单，返回一个问候信息：</p>
<pre class="crayon-plain-tag">type Greeter interface {
	Greet() string
}</pre>
<p>需要提供一个基于net/rpc的实现：</p>
<pre class="crayon-plain-tag">import "net/rpc"

// Here is an implementation that talks over RPC
type GreeterRPC struct{ client *rpc.Client }

func (g *GreeterRPC) Greet() string {
	var resp string
	err := g.client.Call("Plugin.Greet", new(interface{}), &amp;resp)
	if err != nil {
		panic(err)
	}

	return resp
}</pre>
<div class="blog_h3"><span class="graybg">插件接口</span></div>
<p>此结构实现plugin.Plugin接口：</p>
<pre class="crayon-plain-tag">type GreeterPlugin struct {
	// 内嵌业务接口，插件进程会设置此自动，宿主进程则置空
	Impl Greeter
}

// 此方法由插件进程Lazy的调用
func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
	return &amp;GreeterRPCServer{Impl: p.Impl}, nil
}

// 此方法由宿主进程调用
func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
	return &amp;GreeterRPC{client: c}, nil
}

// 这是net/rpc所需要的RPC服务器对象
type GreeterRPCServer struct {
	// This is the real implementation
	Impl Greeter
}
func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error {
	*resp = s.Impl.Greet()
	return nil
}</pre>
<div class="blog_h3"><span class="graybg">宿主进程</span></div>
<p>即插件客户端：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/basic/commons"
	"log"
	"net"
	"os"
)

func main() {
	logger := hclog.New(&amp;hclog.LoggerOptions{
		Name:   "plugin",
		Output: os.Stdout,
		Level:  hclog.Debug,
	})

	// 宿主机调用plugin.NewClient，创建或连接到插件进程
	client := plugin.NewClient(&amp;plugin.ClientConfig{
		HandshakeConfig: handshakeConfig,
		Plugins: map[string]plugin.Plugin{
			// 插件名字到插件的映射关系
			"greeter": &amp;example.GreeterPlugin{},
		},
		// 创建新进程
		Cmd:             exec.Command("./plugin/greeter"),
		// 连接到现有进程
		Reattach: &amp;plugin.ReattachConfig{
			Pid: 2802223,
			Addr: &amp;net.UnixAddr{
				Net:  "unix",
				Name: "/tmp/plugin137476534",
			},
		},
		Logger: logger,
	})
	// 此调用会终止插件子进程的执行，并且进行必要的清理工作（例如收集日志）
	defer client.Kill()

	// 获取RPC客户端对象
	rpcClient, err := client.Client()
	if err != nil {
		log.Fatal(err)
	}

	// 产生具有指定名字的插件的实例
	raw, err := rpcClient.Dispense("greeter")
	if err != nil {
		log.Fatal(err)
	}

	// 插件实例可以强制转型为业务接口
	greeter := raw.(example.Greeter)
	// 像调用本地函数一样调用远程插件，客户端本身是线程安全的，你可以并发的调用业务接口
	fmt.Println(greeter.Greet())
}

// 用于插件和宿主之间的简单握手，用于提升用户体验（而非安全特性）
// 如果握手失败，则提示一个友好的信息
// 可以防止：1、执行错误的插件；2、用户手工启动插件
var handshakeConfig = plugin.HandshakeConfig{
	ProtocolVersion:  1,
	MagicCookieKey:   "BASIC_PLUGIN",
	MagicCookieValue: "hello",
}</pre>
<p>不管是插件客户端还是服务器，都需要（在ClientConfig/ServeConfig中）指定一个一个PluginSet，列出支持的插件。对于服务器，必须指定每个插件的Impl。</p>
<div class="blog_h3"><span class="graybg">插件进程</span></div>
<pre class="crayon-plain-tag">package main

import (
	"os"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/basic/commons"
)

// 业务接口的真实实现
type GreeterHello struct {
	logger hclog.Logger
}

func (g *GreeterHello) Greet() string {
	g.logger.Debug("message from GreeterHello.Greet")
	return "Hello!"
}

// 握手配置
var handshakeConfig = plugin.HandshakeConfig{
	ProtocolVersion:  1,
	MagicCookieKey:   "BASIC_PLUGIN",
	MagicCookieValue: "hello",
}

func main() {
	logger := hclog.New(&amp;hclog.LoggerOptions{
		Level:      hclog.Trace,
		Output:     os.Stderr,
		JSONFormat: true,
	})

	greeter := &amp;GreeterHello{
		logger: logger,
	}
	// 插件集类似于宿主进程，只是插件需要指定Impl字段
	var pluginMap = map[string]plugin.Plugin{
		"greeter": &amp;example.GreeterPlugin{Impl: greeter},
	}

	logger.Debug("message from plugin", "foo", "bar")

	// 启动RPC监听
	plugin.Serve(&amp;plugin.ServeConfig{
		HandshakeConfig: handshakeConfig,
		Plugins:         pluginMap,
	})
}</pre>
<p>服务器调用plugin.Serve方法后，主线程即阻塞。直到客户端调用<pre class="crayon-plain-tag">Dispense</pre>方法请求插件实例时，服务器端才会实例化插件（业务接口的实现）：</p>
<pre class="crayon-plain-tag">func (d *dispenseServer) Dispense(
	name string, response *uint32) error {
	// 从PluginSet中查找
	p, ok := d.plugins[name]
	if !ok {
		return fmt.Errorf("unknown plugin type: %s", name)
	}

	// 调用（下面的那个函数）插件接口的方法
	impl, err := p.Server(d.broker)
	if err != nil {
		return errors.New(err.Error())
	}

	// MuxBroker基于唯一性的ID进行TCP连接的多路复用
	id := d.broker.NextId()
	*response = id

	// 在另外一个协程中处理该请求
	go func() {
		conn, err := d.broker.Accept(id)
		if err != nil {
			log.Printf("[ERR] go-plugin: plugin dispense error: %s: %s", name, err)
			return
		}

		serve(conn, "Plugin", impl)
	}()

	return nil
}

func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
	return &amp;GreeterRPCServer{Impl: p.Impl}, nil
}</pre>
<div class="blog_h2"><span class="graybg">gRPC</span></div>
<p>应当尽量使用grpc而非net/rpc，原因如下：</p>
<ol>
<li>gRPC支持多种语言来实现插件，而net/rpc是Go专有的</li>
<li>在gRPC模式下，go-plugin插件请求通过HTTP2发送，而非私有的yamux</li>
<li>net/rpc使用go类型，尽管比较方便，但却存在潜在的兼容性问题</li>
</ol>
<p>此外需要注意，对于gRPC模式来说，插件进程中只会有单个插件“实例”。对于net/rpc你可能创建多个“实例”。</p>
<p>使用gRPC模式时，你需要：</p>
<ol>
<li>通过Proto文件定义语言中立的接口</li>
<li>从Proto生成gRPC客户端、服务器样板代码</li>
<li>实现<pre class="crayon-plain-tag">plugin.GRPCPlugin</pre>的两个方法，分别调用上面生成的样板代码，获取gRPC客户端、注册gRPC服务实现类</li>
<li>在gRPC服务实现类中，将gRPC接口适配为业务接口</li>
<li>业务接口的实现由插件进程提供</li>
</ol>
<p>样板类模板比较多，有些繁琐。</p>
<div class="blog_h3"><span class="graybg">Proto定义</span></div>
<p>一个简单的键值存储服务：</p>
<pre class="crayon-plain-tag">syntax = "proto3";
package proto;

message GetRequest {
    string key = 1;
}

message GetResponse {
    bytes value = 1;
}

message PutRequest {
    string key = 1;
    bytes value = 2;
}

message Empty {}

service KV {
    rpc Get(GetRequest) returns (GetResponse);
    rpc Put(PutRequest) returns (Empty);
}</pre>
<p>执行命令：<pre class="crayon-plain-tag">protoc -I proto/ proto/kv.proto --go_out=plugins=grpc:proto/</pre> 生成Go代码。</p>
<div class="blog_h3"><span class="graybg">业务接口</span></div>
<pre class="crayon-plain-tag">// Package shared contains shared data between the host and plugins.
package shared

import (
	"context"
	"net/rpc"

	"google.golang.org/grpc"

	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/grpc/proto"
)


// 业务接口
type KV interface {
	Put(key string, value []byte) error
	Get(key string) ([]byte, error)
}</pre>
<div class="blog_h3"><span class="graybg">插件接口</span></div>
<p>这一节的代码被宿主进程，插件进程共享。</p>
<p>gRPC模式下，你需要实现接口plugin.GRPCPlugin，并嵌入plugin.Plugin接口：</p>
<pre class="crayon-plain-tag">type KVGRPCPlugin struct {
	// 需要嵌入插件接口
	plugin.Plugin
	// 具体实现，仅当业务接口实现基于Go时该字段有用
	Impl KV
}</pre>
<p>plugin.GRPCPlugin接口的规格如下，你需要实现两个方法：</p>
<pre class="crayon-plain-tag">type GRPCPlugin interface {
	// 此方法被插件进程调用，插件进程提供一个grpc.Server
	// 你需要向其注册gRPC服务的实现（服务器端存根）
	// 由于gRPC下服务器端是单例模式，因此该方法仅调用一次
	GRPCServer(*GRPCBroker, *grpc.Server) error

	// 此方法被宿主进程调用
	// 你需要返回一个业务接口的实现（客户端存根），此实现直接将请求转给gRPC客户端即可
	// 传入的context对象会在插件进程销毁时取消
	GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
}</pre>
<p>在GRPCServer方法的实现中，你需要向参数提供的grpc.Server<span style="background-color: #c0c0c0;">注册通过Proto文件生成的gRPC服务的Go接口</span>：</p>
<pre class="crayon-plain-tag">type KVServer interface {
	Get(context.Context, *GetRequest) (*GetResponse, error)
	Put(context.Context, *PutRequest) (*Empty, error)
}</pre>
<p>的实现（服务器存根）：</p>
<pre class="crayon-plain-tag">// 在此方法中提供实现：
func (p *KVGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
	proto.RegisterKVServer(s, &amp;KVServerStub{Impl: p.Impl})
	return nil
}

// 实现自动生成的KVServer接口，具体逻辑委托给业务接口KV的实现
type KVServerStub struct {
	// This is the real implementation
	Impl KV
}

// 样板代码
func (m *KVServerStub) Put( ctx context.Context, req *proto.PutRequest) (*proto.Empty, error) {
	return &amp;proto.Empty{}, m.Impl.Put(req.Key, req.Value)
}
func (m *KVServerStub) Get( ctx context.Context, req *proto.GetRequest) (*proto.GetResponse, error) {
	v, err := m.Impl.Get(req.Key)
	return &amp;proto.GetResponse{Value: v}, err
}</pre>
<p>在GRPCClient方法的实现中，你需要<span style="background-color: #c0c0c0;">返回一个业务接口的实现（客户端存根）</span>，此实现只是将请求转发给gRPC服务处理：</p>
<pre class="crayon-plain-tag">func (p *KVGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
	//                         创建gRPC客户端的方法是自动生成的
	return &amp;KVClientStub{client: proto.NewKVClient(c)}, nil
}

// 业务接口KV的实现，通过gRPC客户端转发请求给插件进程
type KVClientStub struct{ client proto.KVClient }

//                   业务接口
func (m *KVClientStub) Put(key string, value []byte) error {
// 转发
	_, err := m.client.Put(context.Background(), &amp;proto.PutRequest{
		Key:   key,
		Value: value,
	})
	return err
}

func (m *KVClientStub) Get(key string) ([]byte, error) {
	resp, err := m.client.Get(context.Background(), &amp;proto.GetRequest{
		Key: key,
	})
	if err != nil {
		return nil, err
	}

	return resp.Value, nil
}</pre>
<div class="blog_h3"><span class="graybg">宿主进程</span></div>
<p>使用gRPC方式时，只需要设置AllowedProtocols，其余的和netrpc模式没有区别。</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"

	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/grpc/shared"
)

func main() {
	log.SetOutput(ioutil.Discard)

	// 创建插件进程
	client := plugin.NewClient(&amp;plugin.ClientConfig{
		HandshakeConfig: plugin.HandshakeConfig{
			ProtocolVersion:  1,
			MagicCookieKey:   "BASIC_PLUGIN",
			MagicCookieValue: "hello",
		},
		Plugins:         map[string]plugin.Plugin{
			"kv_grpc": &amp;KVGRPCPlugin{},
		},
		Cmd:             exec.Command("sh", "-c", os.Getenv("KV_PLUGIN")),
  	 	// 允许的协议类型，默认情况下允许netrpc
		AllowedProtocols: []plugin.Protocol{
			plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
	})
	defer client.Kill()

	// 获取RPC客户端
	rpcClient, err := client.Client()
	if err != nil {
		fmt.Println("Error:", err.Error())
		os.Exit(1)
	}

	// 得到插件实例
	raw, err := rpcClient.Dispense("kv_grpc")
	if err != nil {
		fmt.Println("Error:", err.Error())
		os.Exit(1)
	}

	// 插件实例转换为业务接口
	// ...
}</pre>
<div class="blog_h3"><span class="graybg">插件进程</span></div>
<p>和netrpc方式也没什么差别，只需要指定GRPCServer，提供创建gRPC服务器的函数即可：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"io/ioutil"

	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/grpc/shared"
)

// 这里是KV的真实实现
type KV struct{}

func (KV) Put(key string, value []byte) error {
	value = []byte(fmt.Sprintf("%s\n\nWritten from plugin-go-grpc", string(value)))
	return ioutil.WriteFile("kv_"+key, value, 0644)
}

func (KV) Get(key string) ([]byte, error) {
	return ioutil.ReadFile("kv_" + key)
}

func main() {
	plugin.Serve(&amp;plugin.ServeConfig{
		HandshakeConfig: plugin.HandshakeConfig{
			ProtocolVersion:  1,
			MagicCookieKey:   "BASIC_PLUGIN",
			MagicCookieValue: "hello",
		},
		Plugins: map[string]plugin.Plugin{
			"kv": &amp;shared.KVGRPCPlugin{Impl: &amp;KV{}},
		},

		// 该字段不为nil，则基于gRPC协议进行服务
		GRPCServer: plugin.DefaultGRPCServer,
	})
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-plugin-over-grpc">基于本地gRPC的Go插件系统</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/go-plugin-over-grpc/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Ginkgo学习笔记</title>
		<link>https://blog.gmem.cc/ginkgo-study-note</link>
		<comments>https://blog.gmem.cc/ginkgo-study-note#comments</comments>
		<pubDate>Tue, 19 Nov 2019 07:06:04 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[单元测试]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=29913</guid>
		<description><![CDATA[<p>简介 Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发（BDD， Behavior-Driven Development）风格的测试框架，通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。 Ginkgo集成了Go语言的测试机制，你可以通过[crayon-69d3148a633d6274278522-i/]来运行Ginkgo测试套件。 Ginkgo 安装 [crayon-69d3148a633db933327767/] 起步 创建套件 假设我们想给books包编写Ginkgo测试，则首先需要使用命令创建一个Ginkgo test suite： [crayon-69d3148a633dd635779814/] 上述命令会生成文件： [crayon-69d3148a633df783196918/] 现在，使用命令[crayon-69d3148a633e2553431301-i/]或者[crayon-69d3148a633e4067682487-i/]即可执行测试套件。 添加Spec 上面的空测试套件没有什么价值，我们需要在此套接下编写测试（Spec）。虽然可以在books_suite_test.go中编写测试，但是推荐分离到独立的文件中，特别是包中有多个需要被测试的源文件的情况下。 <a class="read-more" href="https://blog.gmem.cc/ginkgo-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ginkgo-study-note">Ginkgo学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发（BDD， Behavior-Driven Development）风格的测试框架，通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。</p>
<p>Ginkgo集成了Go语言的测试机制，你可以通过<pre class="crayon-plain-tag">go test</pre>来运行Ginkgo测试套件。</p>
<div class="blog_h1"><span class="graybg">Ginkgo</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">go get -u github.com/onsi/ginkgo/ginkgo</pre>
<div class="blog_h2"><span class="graybg">起步</span></div>
<div class="blog_h3"><span class="graybg">创建套件</span></div>
<p>假设我们想给books包编写Ginkgo测试，则首先需要使用命令创建一个Ginkgo test suite：</p>
<pre class="crayon-plain-tag">cd pkg/books
ginkgo bootstrap</pre>
<p>上述命令会生成文件：</p>
<pre class="crayon-plain-tag">package books_test

import (
    // 使用点号导入，把这两个包导入到当前命名空间
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    "testing"
)

func TestBooks(t *testing.T) {
    // 将Ginkgo的Fail函数传递给Gomega，Fail函数用于标记测试失败，这是Ginkgo和Gomega唯一的交互点
    // 如果Gomega断言失败，就会调用Fail进行处理
    RegisterFailHandler(Fail)

    // 启动测试套件
    RunSpecs(t, "Books Suite")
}</pre>
<p>现在，使用命令<pre class="crayon-plain-tag">ginkgo</pre>或者<pre class="crayon-plain-tag">go test</pre>即可执行测试套件。</p>
<div class="blog_h3"><span class="graybg">添加Spec</span></div>
<p>上面的空测试套件没有什么价值，我们需要在此套接下编写测试（Spec）。虽然可以在books_suite_test.go中编写测试，但是推荐<span style="background-color: #c0c0c0;">分离到独立的文件</span>中，特别是包中有多个需要被测试的源文件的情况下。</p>
<p>执行命令<pre class="crayon-plain-tag">ginkgo generate book</pre>可以为源文件book.go生成测试：</p>
<pre class="crayon-plain-tag">package books_test

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	// 为了方便，被测试包被导入当前命名空间
	. "ginkgo-study/pkg/books"
)

// 顶级的Describe容器

// Describe块用于组织Specs，其中可以包含任意数量的：
//    BeforeEach：在Spec（It块）运行之前执行，嵌套Describe时最外层BeforeEach先执行
//    AfterEach：在Spec运行之后执行，嵌套Describe时最内层AfterEach先执行
//    JustBeforeEach：在It块，所有BeforeEach之后执行
//    Measurement

// 可以在Describe块内嵌套Describe、Context、When块
var _ = Describe("Book", func() {

})</pre>
<p>我们可以添加一些Specs：</p>
<pre class="crayon-plain-tag">// 使用Describe、Context容器来组织Spec
var _ = Describe("Book", func() {
	var (
		// 通过闭包在BeforeEach和It之间共享数据
		longBook  Book
		shortBook Book
	)
	// 此函数用于初始化Spec的状态，在It块之前运行。如果存在嵌套Describe，则最
	// 外面的BeforeEach最先运行
	BeforeEach(func() {
		longBook = Book{
			Title:  "Les Miserables",
			Author: "Victor Hugo",
			Pages:  1488,
		}

		shortBook = Book{
			Title:  "Fox In Socks",
			Author: "Dr. Seuss",
			Pages:  24,
		}
	})

	Describe("Categorizing book length", func() {
		Context("With more than 300 pages", func() {
			// 通过It来创建一个Spec
			It("should be a novel", func() {
				// Gomega的Expect用于断言
				Expect(longBook.CategoryByLength()).To(Equal("NOVEL"))
			})
		})

		Context("With fewer than 300 pages", func() {
			It("should be a short story", func() {
				Expect(shortBook.CategoryByLength()).To(Equal("SHORT STORY"))
			})
		})
	})
})</pre>
<div class="blog_h3"><span class="graybg">断言失败</span></div>
<p>除了调用Gomega之外，你还可以调用Fail函数直接断言失败：</p>
<pre class="crayon-plain-tag">Fail("Failure reason")</pre>
<p>Fail会记录当前进行的测试，并且触发panic，当前Spec的后续断言不会再进行。</p>
<p>通常情况下Ginkgo会<span style="background-color: #c0c0c0;">从panic中恢复，并继续下一个测试</span>。但是，如果你启动了一个Goroutine，并在其中触发了断言失败，则不会自动恢复，必须手工调用GinkgoRecover：</p>
<pre class="crayon-plain-tag">It("panics in a goroutine", func(done Done) {
    go func() {
        // 如果doSomething返回false则下面的defer会确保从panic中恢复
        defer GinkgoRecover()
        // Ω和Expect功能相同
        Ω(doSomething()).Should(BeTrue())

        // 在Goroutine中需要关闭done通道
        close(done)
    }()
})</pre>
<div class="blog_h3"><span class="graybg">记录日志</span></div>
<p>全局的GinkgoWriter可以用于写日志。默认情况下GinkgoWriter仅仅在测试失败时将日志Dump到标准输出，以冗长模式（<pre class="crayon-plain-tag">ginkgo -v</pre> 或 <pre class="crayon-plain-tag">go test -ginkgo.v</pre>）运行Ginkgo时则会立即输出。</p>
<p>如果通过Ctrl + C中断测试，则Ginkgo会立即输出写入到GinkgoWriter的内容。联用<pre class="crayon-plain-tag">--progress</pre>则Ginkgo会在BeforeEach/It/AfterEach之前输出通知到GinkgoWriter，这个特性便于诊断卡住的测试。</p>
<div class="blog_h3"><span class="graybg">传递参数</span></div>
<p>直接使用flag包即可：</p>
<pre class="crayon-plain-tag">var myFlag string
func init() {
    flag.StringVar(&amp;myFlag, "myFlag", "defaultvalue", "myFlag is used to control my behavior")
}</pre>
<p>执行测试时使用<pre class="crayon-plain-tag">ginkgo -- --myFlag=xxx</pre>传递参数。</p>
<div class="blog_h2"><span class="graybg">测试的结构</span></div>
<div class="blog_h3"><span class="graybg">It</span></div>
<p>你可以在Describe、Context这两种容器块内编写Spec，每个Spec写在It块中。</p>
<p>为了贴合自然语言，可以使用It的别名Specify：</p>
<pre class="crayon-plain-tag">Describe("The foobar service", func() {
  Context("when calling Foo()", func() {
    Context("when no ID is provided", func() {
      // 应该返回ErrNoID错误
      Specify("an ErrNoID error is returned", func() {
      })
    })
  })
})</pre>
<div class="blog_h3"><span class="graybg">BeforeEach</span></div>
<p>多个Spec共享的、测试准备逻辑，可以放到BeforeEach块中。</p>
<p>在BeforeEach、AfterEach块中进行断言是允许的。</p>
<p>存在容器嵌套时，最外层BeforeEach先运行。</p>
<div class="blog_h3"><span class="graybg">AfterEach</span></div>
<p>多个Spec共享的、测试清理逻辑，可以放到AfterEach块中。存在容器嵌套时，最内层AfterEach先运行。</p>
<div class="blog_h3"><span class="graybg">Describe/Context</span></div>
<p>两者的区别：</p>
<ol>
<li>Describe用于描述你的代码的一个行为</li>
<li>Context用于区分上述行为的不同情况，通常为参数不同导致</li>
</ol>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">// 这是关于Book服务测试
var _ = Describe("Book", func() {
    var (
        book Book
        err error
    )

    BeforeEach(func() {
        book, err = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`)
    })
    // 测试加载Book行为
    Describe("loading from JSON", func() {
        // 如果正常解析JSON
        Context("when the JSON parses succesfully", func() {
            It("should populate the fields correctly", func() {
                // 期望                相等
                Expect(book.Title).To(Equal("Les Miserables"))
                Expect(book.Author).To(Equal("Victor Hugo"))
                Expect(book.Pages).To(Equal(1488))
            })

            It("should not error", func() {
                // 期望      没有发生错误
                Expect(err).NotTo(HaveOccurred())
            })
        })
        // 如果无法解析JSON
        Context("when the JSON fails to parse", func() {
            BeforeEach(func() {
                // 这是一个BDD反模式，可以用JustBeforeEach
                book, err = NewBookFromJSON(`{
                    "title":"Les Miserables",
                    "author":"Victor Hugo",
                    "pages":1488oops
                }`)
            })

            It("should return the zero-value for the book", func() {
                // 期望          为零
                Expect(book).To(BeZero())
            })

            It("should error", func() {
                // 期望        发生了错误
                Expect(err).To(HaveOccurred())
            })
        })
    })

    Describe("Extracting the author's last name", func() {
        It("should correctly identify and return the last name", func() {
            Expect(book.AuthorLastName()).To(Equal("Hugo"))
        })
    })
})</pre>
<div class="blog_h3"><span class="graybg">JustBeforeEach</span></div>
<p>上面的例子中，内层Spec需要尝试从无效JSON创建Book，因此它调用NewBookFromJSON对book变量进行覆盖。这种做法是推荐的，应该使用JustBeforeEach，这种块在任何BeforeEach执行完毕后执行：</p>
<pre class="crayon-plain-tag">var _ = Describe("Book", func() {
    var (
        book Book
        err error
        json string
    )
    // 准备默认JSON
    BeforeEach(func() {
        json = `{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`
    })

    JustBeforeEach(func() {
        // 按需，根据默认数据/无效JSON创建book，避免NewBookFromJSON的重复调用（如果代价很高的话……）
        book, err = NewBookFromJSON(json)
    })

    Describe("loading from JSON", func() {
        Context("when the JSON parses succesfully", func() {
        })

        Context("when the JSON fails to parse", func() {
            BeforeEach(func() {
                // 覆盖默认JSON为无效JSON
                json = `{
                    "title":"Les Miserables",
                    "author":"Victor Hugo",
                    "pages":1488oops
                }`
            })
        })
    })
})</pre>
<p>在上面的例子中，JustBeforeEach<span style="background-color: #c0c0c0;">解耦了创建（Creation）和配置（Configuration）</span>这两个阶段。</p>
<div class="blog_h3"><span class="graybg">JustAfterEach</span></div>
<p>紧跟着It之后运行，在所有AfterEach执行之前。</p>
<div class="blog_h3"><span class="graybg">BeforeSuite/AfterSuite</span></div>
<p>在整个测试套件执行之前/之后，进行准备/清理。和套件代码写在一起：</p>
<pre class="crayon-plain-tag">func TestBooks(t *testing.T) {
    RegisterFailHandler(Fail)

    RunSpecs(t, "Books Suite")
}

var _ = BeforeSuite(func() {
    dbClient = db.NewClient()
    err = dbClient.Connect(dbRunner.Address())
    Expect(err).NotTo(HaveOccurred())
})

var _ = AfterSuite(func() {
    dbClient.Cleanup()
})</pre>
<p>这两个块都支持异步执行，只需要给函数传递一个Done参数即可。 </p>
<div class="blog_h3"><span class="graybg">By</span></div>
<p>此块用于给逻辑复杂的块添加文档：</p>
<pre class="crayon-plain-tag">var _ = Describe("Browsing the library", func() {
    BeforeEach(func() {
        By("Fetching a token and logging in")
    })

    It("should be a pleasant experience", func() {
        By("Entering an aisle")
    })
})</pre>
<p>传递给By的字符串会发送给GinkgoWriter，如果测试失败你可以看到。</p>
<p>你可以传递一个可选的函数给By，此函数会立即执行。</p>
<div class="blog_h2"><span class="graybg">性能测试</span></div>
<p>使用Measure块可以进行性能测试，所有It能够出现的地方，都可以使用Measure。和It一样，Measure会生成一个新的Spec。</p>
<p>传递给Measure的闭包函数必须具有Benchmarker入参：</p>
<pre class="crayon-plain-tag">Measure("it should do something hard efficiently", func(b Benchmarker) {
    // 执行一段逻辑并即时
    runtime := b.Time("runtime", func() {
        output := SomethingHard()
        Expect(output).To(Equal(17))
    })

    // 断言 执行时间             小于 0.2 秒
    Ω(runtime.Seconds()).Should(BeNumerically("&lt;", 0.2), "SomethingHard() shouldn't take too long.")

    // 录制任意数据
    b.RecordValue("disk usage (in MB)", HowMuchDiskSpaceDidYouUse())
}, 10)</pre>
<p>执行时间、你录制的任意数据的最小、最大、平均值均会在测试完毕后打印出来。</p>
<div class="blog_h2"><span class="graybg">CLI</span></div>
<div class="blog_h3"><span class="graybg">运行测试</span></div>
<pre class="crayon-plain-tag"># 运行当前目录中的测试
ginkgo
# 运行其它目录中的测试
ginkgo /path/to/package /path/to/other/package ...

# 递归运行所有子目录中的测试
ginkgo -r ...</pre>
<div class="blog_h3"><span class="graybg">传递参数</span></div>
<p>传递参数给测试套件：  </p>
<pre class="crayon-plain-tag">ginkgo -- PASS-THROUGHS-ARGS</pre>
<div class="blog_h3"><span class="graybg">跳过某些包</span></div>
<pre class="crayon-plain-tag"># 跳过某些包
ginkgo -skipPackage=PACKAGES,TO,SKIP</pre>
<div class="blog_h3"><span class="graybg">超时控制</span></div>
<p>选项<pre class="crayon-plain-tag">-timeout</pre>用于控制套件的最大运行时间，如果超过此时间仍然没有完成，认为测试失败。默认24小时。</p>
<div class="blog_h3"><span class="graybg">调试信息</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--reportPassed</td>
<td>打印通过的测试的详细信息</td>
</tr>
<tr>
<td>--v</td>
<td>冗长模式</td>
</tr>
<tr>
<td>--trace</td>
<td>打印所有错误的调用栈</td>
</tr>
<tr>
<td>--progress</td>
<td>打印进度信息</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">其它选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-race</td>
<td>启用竞态条件检测</td>
</tr>
<tr>
<td>-cover</td>
<td>启用覆盖率测试</td>
</tr>
<tr>
<td>-tags</td>
<td>指定编译器标记</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Spec Runner</span></div>
<div class="blog_h3"><span class="graybg">Pending Spec</span></div>
<p>你可以标记一个Spec或容器为Pending，这样默认<span style="background-color: #c0c0c0;">情况下不会运行</span>它们。定义块时使用P或X前缀：</p>
<pre class="crayon-plain-tag">PDescribe("some behavior", func() { ... })
PContext("some scenario", func() { ... })
PIt("some assertion")
PMeasure("some measurement")

XDescribe("some behavior", func() { ... })
XContext("some scenario", func() { ... })
XIt("some assertion")
XMeasure("some measurement")</pre>
<p>默认情况下Ginkgo会为每个Pending的Spec打印描述信息，使用命令行选项<pre class="crayon-plain-tag">--noisyPendings=false</pre>禁止该行为。 </p>
<div class="blog_h3"><span class="graybg">Skiping Spec</span></div>
<p>P或X前缀会在编译期将Spec标记为Pending，你也可以在运行期跳过特定的Spec：</p>
<pre class="crayon-plain-tag">It("should do something, if it can", func() {
    if !someCondition {
        // 跳过此Spec，不需要Return语句
        Skip("special condition wasn't met")
    }
})</pre>
<div class="blog_h3"><span class="graybg">Focused Specs</span></div>
<p>一个很常见的需求是，可以选择运行Spec的一个子集。Ginkgo提供两种机制满足此需求：</p>
<ol>
<li> 将容器或Spec标记为Focused，这样默认情况下Ginkgo仅仅运行Focused Spec：<br />
<pre class="crayon-plain-tag">FDescribe("some behavior", func() { ... })
 FContext("some scenario", func() { ... })
 FIt("some assertion", func() { ... })</pre>
</li>
</ol>
<ol>
<li>
<p>在命令行中传递正则式：<pre class="crayon-plain-tag">--focus=REGEXP</pre> 或/和 <pre class="crayon-plain-tag">--skip=REGEXP</pre>，则Ginkgo仅仅运行/跳过匹配的Spec</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Parallel Specs</span></div>
<p>Ginkgo支持并行的运行Spec，它实现方式是，创建go test子进程并在其中运行共享队列中的Spec。</p>
<p>使用<pre class="crayon-plain-tag">ginkgo -p</pre>可以启用并行测试，Ginkgo会自动创建适当数量的节点（进程）。你也可以指定节点数量：<pre class="crayon-plain-tag">ginkgo -nodes=N</pre>。</p>
<p>如果你的测试代码需要和外部进程交互，或者创建外部进程，在并行测试上下文中需要谨慎的处理。最简单的方式是在BeforeSuite方法中为每个节点创建外部资源。</p>
<p>如果所有Spec需要共享一个外部进程，则可以利用SynchronizedBeforeSuite、SynchronizedAfterSuite：</p>
<pre class="crayon-plain-tag">var _ = SynchronizedBeforeSuite(func() []byte {
    // 在第一个节点中执行
    port := 4000 + config.GinkgoConfig.ParallelNode

    dbRunner = db.NewRunner()
    err := dbRunner.Start(port)
    Expect(err).NotTo(HaveOccurred())

    return []byte(dbRunner.Address())
}, func(data []byte) {
    // 在所有节点中执行
    dbAddress := string(data)

    dbClient = db.NewClient()
    err = dbClient.Connect(dbAddress)
    Expect(err).NotTo(HaveOccurred())
})</pre>
<p>上面的例子，为所有节点创建共享的数据库，然后为每个节点创建独占的客户端。 SynchronizedAfterSuite的回调顺序则正好相反：</p>
<pre class="crayon-plain-tag">var _ = SynchronizedAfterSuite(func() {
    // 所有节点
    dbClient.Cleanup()
}, func() {
    // 第一个节点
    dbRunner.Stop()
}) </pre>
<div class="blog_h1"><span class="graybg">Gomega</span></div>
<p>这时Ginkgo推荐使用的断言（Matcher）库。</p>
<div class="blog_h2"><span class="graybg">联用</span></div>
<div class="blog_h3"><span class="graybg">和Ginkgo</span></div>
<p>注册Fail处理器即可：</p>
<pre class="crayon-plain-tag">gomega.RegisterFailHandler(ginkgo.Fail)</pre>
<div class="blog_h3"><span class="graybg">和Go测试框架</span></div>
<pre class="crayon-plain-tag">func TestFarmHasCow(t *testing.T) {
    // 创建Gomega对象
    g := NewGomegaWithT(t)

    f := farm.New([]string{"Cow", "Horse"})
    // 进行断言
    g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
}</pre>
<div class="blog_h2"><span class="graybg">断言</span></div>
<div class="blog_h3"><span class="graybg">Ω/Expect</span></div>
<p>两种断言语法本质是一样的，只是命名风格有些不同：</p>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(Equal(EXPECTED))
Expect(ACTUAL).To(Equal(EXPECTED))

Ω(ACTUAL).ShouldNot(Equal(EXPECTED))
Expect(ACTUAL).NotTo(Equal(EXPECTED))
Expect(ACTUAL).ToNot(Equal(EXPECTED))</pre>
<div class="blog_h3"><span class="graybg">错误处理</span></div>
<p>对于返回多个值的函数：</p>
<pre class="crayon-plain-tag">func DoSomethingHard() (string, error) {}

result, err := DoSomethingHard()
// 断言没有发生错误
Ω(err).ShouldNot(HaveOccurred())
Ω(result).Should(Equal("foo"))</pre>
<p>对于仅仅返回一个error的函数： </p>
<pre class="crayon-plain-tag">func DoSomethingHard() (string, error) {}

Ω(DoSomethingSimple()).Should(Succeed())</pre>
<div class="blog_h3"><span class="graybg">断言注解</span></div>
<p>进行断言时，可以提供格式化字符串，这样断言失败可以方便的知道原因：</p>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(Equal(EXPECTED), "My annotation %d", foo)

Expect(ACTUAL).To(Equal(EXPECTED), "My annotation %d", foo)

Expect(ACTUAL).To(Equal(EXPECTED), func() string { return "My annotation" })</pre>
<div class="blog_h3"><span class="graybg">简化输出</span></div>
<p>断言失败时，Gomega打印牵涉到断言的对象的递归信息，输出可能很冗长。</p>
<p>format包提供了一些全局变量，调整这些变量可以简化输出。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">变量 = 默认值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>format.MaxDepth = 10</td>
<td>打印对象嵌套属性的最大深度</td>
</tr>
<tr>
<td>format.UseStringerRepresentation = false</td>
<td>
<p>默认情况下，Gomega不会调用Stringer.String()或GoStringer.GoString()方法来打印对象的字符串表示</p>
<p>字符串表示通常人类可读但是信息量较小</p>
<p>设置为true则打印字符串表示，可以简化输出</p>
</td>
</tr>
<tr>
<td>format.PrintContextObjects = false</td>
<td>默认情况下，Gomega不会打印context.Context接口的内容，因为通常非常冗长</td>
</tr>
<tr>
<td>format.TruncatedDiff = true</td>
<td>截断长字符串，仅仅打印差异</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">异步断言</span></div>
<p>Gomega提供了两个函数，用于异步断言。</p>
<p>传递给Eventually、Consistently的函数，如果返回多个值，则第一个返回值用于匹配，<span style="background-color: #c0c0c0;">其它值断言为nil或零值</span>。</p>
<div class="blog_h3"><span class="graybg">Eventually</span></div>
<p>阻塞并轮询参数，直到能通过断言：</p>
<pre class="crayon-plain-tag">// 参数是闭包，调用函数
Eventually(func() []int {
    return thing.SliceImMonitoring
}).Should(HaveLen(2))

// 参数是通道，读取通道
Eventually(channel).Should(BeClosed())
Eventually(channel).Should(Receive())

// 参数也可以是普通变量，读取变量
Eventually(myInstance.FetchNameFromNetwork).Should(Equal("archibald"))

// 可以和gexec包的Session配合
Eventually(session).Should(gexec.Exit(0)) // 命令最终应当以0退出
Eventually(session.Out).Should(Say("Splines reticulated")) // 检查标准输出</pre>
<p>可以指定超时、轮询间隔：</p>
<pre class="crayon-plain-tag">Eventually(func() []int {
    return thing.SliceImMonitoring
}, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2))</pre>
<div class="blog_h3"><span class="graybg">Consistently</span></div>
<p>检查断言是否在一定时间段内总是通过：</p>
<pre class="crayon-plain-tag">Consistently(func() []int {
    return thing.MemoryUsage()
}, DURATION, POLLING_INTERVAL).Should(BeNumerically("&lt;", 10))</pre>
<p>Consistently也可以用来断言最终不会发生的事件，例如下面的例子：</p>
<pre class="crayon-plain-tag">Consistently(channel).ShouldNot(Receive())</pre>
<div class="blog_h3"><span class="graybg">修改默认间隔</span></div>
<p>默认情况下，Eventually每10ms轮询一次，持续1s。Consistently每10ms轮询一次，持续100ms。调用下面的函数修改这些默认值：</p>
<pre class="crayon-plain-tag">SetDefaultEventuallyTimeout(t time.Duration)
SetDefaultEventuallyPollingInterval(t time.Duration)
SetDefaultConsistentlyDuration(t time.Duration)
SetDefaultConsistentlyPollingInterval(t time.Duration)</pre>
<p>这些调用会影响整个测试套件。</p>
<div class="blog_h2"><span class="graybg">内置Matcher</span></div>
<div class="blog_h3"><span class="graybg">相等性</span> </div>
<pre class="crayon-plain-tag">// 使用reflect.DeepEqual进行比较
// 如果ACTUAL和EXPECTED都为nil，断言会失败
Ω(ACTUAL).Should(Equal(EXPECTED))

// 先把ACTUAL转换为EXPECTED的类型，然后使用reflect.DeepEqual进行比较
// 应当避免用来比较数字
Ω(ACTUAL).Should(BeEquivalentTo(EXPECTED))

// 使用 == 进行比较
BeIdenticalTo(expected interface{})</pre>
<div class="blog_h3"><span class="graybg">接口相容</span></div>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(BeAssignableToTypeOf(EXPECTED interface))</pre>
<div class="blog_h3"><span class="graybg">空值/零值</span></div>
<pre class="crayon-plain-tag">// 断言ACTUAL为Nil
Ω(ACTUAL).Should(BeNil())

// 断言ACTUAL为它的类型的零值，或者是Nil
Ω(ACTUAL).Should(BeZero())</pre>
<div class="blog_h3"><span class="graybg">布尔值</span></div>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(BeTrue())
Ω(ACTUAL).Should(BeFalse())</pre>
<div class="blog_h3"><span class="graybg">错误</span></div>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(HaveOccurred())

err := SomethingThatMightFail()
// 没有错误
Ω(err).ShouldNot(HaveOccurred())


// 如果ACTUAL为Nil则断言成功
Ω(ACTUAL).Should(Succeed())</pre>
<p>可以对错误进行细粒度的匹配：</p>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(MatchError(EXPECTED))</pre>
<p>上面的EXPECTED可以是：</p>
<ol>
<li>字符串：则断言ACTUAL.Error()与之相等</li>
<li>Matcher：则断言ACTUAL.Error()与之进行匹配</li>
<li>error：则ACTUAL和error基于reflect.DeepEqual()进行比较</li>
<li>实现了error接口的非Nil指针，调用<pre class="crayon-plain-tag">errors.As(ACTUAL, EXPECTED)</pre>进行检查</li>
</ol>
<p>不符合以上条件的EXPECTED是不允许的。</p>
<div class="blog_h3"><span class="graybg">通道</span></div>
<pre class="crayon-plain-tag">// 断言通道是否关闭
// Gomega会尝试读取通道进行判断，因此你需要注意：
//    如果是缓冲通道，你需要先将通道读干净
//    如果你后续需要再次读取通道，注意此断言的影响
Ω(ACTUAL).Should(BeClosed())
Ω(ACTUAL).ShouldNot(BeClosed())


// 断言能够从通道里面读取到消息
// 此断言会立即返回，如果通道已经关闭，则下面的断言失败
Ω(ACTUAL).Should(Receive(&lt;optionalPointer&gt;))



// 断言能够无阻塞的发送消息
Ω(ACTUAL).Should(BeSent(VALUE))</pre>
<div class="blog_h3"><span class="graybg">文件</span></div>
<pre class="crayon-plain-tag">// 文件或目录存在
Ω(ACTUAL).Should(BeAnExistingFile())
// 断言是普通文件
Ω(ACTUAL).Should(BeARegularFile())
// 断言是目录
BeADirectory</pre>
<div class="blog_h3"><span class="graybg">字符串</span></div>
<pre class="crayon-plain-tag">// 子串判断                        fmt.Sprintf(STRING, ARGS...)
Ω(ACTUAL).Should(ContainSubstring(STRING, ARGS...))

// 前缀判断
Ω(ACTUAL).Should(HavePrefix(STRING, ARGS...))

// 后缀判断
Ω(ACTUAL).Should(HaveSuffix(STRING, ARGS...))


// 正则式匹配
Ω(ACTUAL).Should(MatchRegexp(STRING, ARGS...))</pre>
<div class="blog_h3"><span class="graybg">JSON/XML/YML</span></div>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(MatchJSON(EXPECTED))
Ω(ACTUAL).Should(MatchXML(EXPECTED))
Ω(ACTUAL).Should(MatchYAML(EXPECTED))</pre>
<p>ACTUAL、EXPECTED可以是string、[]byte、Stringer。如果两者转换为对象是reflect.DeepEqual的则匹配。</p>
<div class="blog_h3"><span class="graybg">集合</span></div>
<p>string, array, map, chan, slice都属于集合。</p>
<pre class="crayon-plain-tag">// 断言为空
Ω(ACTUAL).Should(BeEmpty())

// 断言长度
Ω(ACTUAL).Should(HaveLen(INT))

// 断言容量
Ω(ACTUAL).Should(HaveCap(INT))

// 断言包含元素
Ω(ACTUAL).Should(ContainElement(ELEMENT))

// 断言等于                   其中之一
Ω(ACTUAL).Should(BeElementOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))


// 断言元素相同，不考虑顺序
Ω(ACTUAL).Should(ConsistOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))
Ω(ACTUAL).Should(ConsistOf([]SOME_TYPE{ELEMENT1, ELEMENT2, ELEMENT3, ...}))

// 断言存在指定的键，仅用于map
Ω(ACTUAL).Should(HaveKey(KEY))
// 断言存在指定的键值对，仅用于map
Ω(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE))</pre>
<div class="blog_h3"><span class="graybg">数字/时间</span></div>
<pre class="crayon-plain-tag">// 断言数字意义（类型不感知）上的相等
Ω(ACTUAL).Should(BeNumerically("==", EXPECTED))

// 断言相似，无差不超过THRESHOLD（默认1e-8）
Ω(ACTUAL).Should(BeNumerically("~", EXPECTED, &lt;THRESHOLD&gt;))


Ω(ACTUAL).Should(BeNumerically("&gt;", EXPECTED))
Ω(ACTUAL).Should(BeNumerically("&gt;=", EXPECTED))
Ω(ACTUAL).Should(BeNumerically("&lt;", EXPECTED))
Ω(ACTUAL).Should(BeNumerically("&lt;=", EXPECTED))

Ω(number).Should(BeBetween(0, 10))</pre>
<p>比较时间时使用BeTemporally函数，和BeNumerically类似。 </p>
<div class="blog_h3"><span class="graybg">Panic</span></div>
<p>断言会发生Panic：</p>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(Panic())</pre>
<div class="blog_h3"><span class="graybg">And/Or</span></div>
<pre class="crayon-plain-tag">Expect(number).To(SatisfyAll(
            BeNumerically("&gt;", 0),
            BeNumerically("&lt;", 10)))
// 或者
Expect(msg).To(And(
            Equal("Success"),
            MatchRegexp(`^Error .+$`)))



Ω(ACTUAL).Should(SatisfyAny(MATCHER1, MATCHER2, ...))
// 或者
Ω(ACTUAL).Should(Or(MATCHER1, MATCHER2, ...))</pre>
<div class="blog_h2"><span class="graybg">自定义Matcher</span></div>
<p>如果内置Matcher无法满足需要，你可以实现接口：</p>
<pre class="crayon-plain-tag">type GomegaMatcher interface {
    Match(actual interface{}) (success bool, err error)
    FailureMessage(actual interface{}) (message string)
    NegatedFailureMessage(actual interface{}) (message string)
}</pre>
<div class="blog_h2"><span class="graybg">辅助工具</span></div>
<div class="blog_h3"><span class="graybg">ghttp</span></div>
<p>用于测试HTTP客户端，此包提供了Mock HTTP服务器的能力。</p>
<div class="blog_h3"><span class="graybg">gbytes</span></div>
<p>gbytes.Buffer实现了接口io.WriteCloser，能够捕获到内存缓冲的输入。配合使用<pre class="crayon-plain-tag">gbytes.Say</pre>能够对流数据进行有序的断言。</p>
<div class="blog_h3"><span class="graybg">gexec</span></div>
<p>简化了外部进程的测试，可以：</p>
<ol>
<li>编译Go二进制文件</li>
<li>启动外部进程</li>
<li>发送信号并等待外部进程退出</li>
<li>基于退出码进行断言</li>
<li>将输出流导入到gbytes.Buffer进行断言</li>
</ol>
<div class="blog_h3"><span class="graybg">gstruct</span></div>
<p>此包用于测试复杂的Go结构，提供了结构、切片、映射、指针相关的Matcher。</p>
<p>对所有字段进行断言：</p>
<pre class="crayon-plain-tag">actual := struct{
    A int
    B bool
    C string
}{5, true, "foo"}
Expect(actual).To(MatchAllFields(Fields{
    "A": BeNumerically("&lt;", 10),
    "B": BeTrue(),
    "C": Equal("foo"),
})</pre>
<p>不处理某些字段： </p>
<pre class="crayon-plain-tag">Expect(actual).To(MatchFields(IgnoreExtras, Fields{
    "A": BeNumerically("&lt;", 10),
    "B": BeTrue(),
    // 忽略C字段
})


Expect(actual).To(MatchFields(IgnoreMissing, Fields{
    "A": BeNumerically("&lt;", 10),
    "B": BeTrue(),
    "C": Equal("foo"),
    "D": Equal("bar"), // 忽略多余字段
})</pre>
<p>一个复杂的例子：</p>
<pre class="crayon-plain-tag">coreID := func(element interface{}) string {
    return strconv.Itoa(element.(CoreStats).Index)
}
Expect(actual).To(MatchAllFields(Fields{
    // 忽略此字段
    "Name":      Ignore(),
    // 时间断言
    "StartTime": BeTemporally("&gt;=", time.Now().Add(-100 * time.Hour)),
    //     解引用后再断言
    "CPU": PointTo(MatchAllFields(Fields{
        "Time":                 BeTemporally("&gt;=", time.Now().Add(-time.Hour)),
        "UsageNanoCores":       BeNumerically("~", 1E9, 1E8),
        "UsageCoreNanoSeconds": BeNumerically("&gt;", 1E6),
        //       包含匹配的元素， 抽取ID的函数
        "Cores": MatchElements(coreID, IgnoreExtras, Elements{
            // ID: Matcher
            "0": MatchAllFields(Fields{
                Index: Ignore(),
                "UsageNanoCores":       BeNumerically("&lt;", 1E9),
                "UsageCoreNanoSeconds": BeNumerically("&gt;", 1E5),
            }),
            "1": MatchAllFields(Fields{
                Index: Ignore(),
                "UsageNanoCores":       BeNumerically("&lt;", 1E9),
                "UsageCoreNanoSeconds": BeNumerically("&gt;", 1E5),
            }),
        }),
    }))
    "Logs":               m.Ignore(),
}))</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ginkgo-study-note">Ginkgo学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ginkgo-study-note/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go应用性能剖析</title>
		<link>https://blog.gmem.cc/go-program-profiling</link>
		<comments>https://blog.gmem.cc/go-program-profiling#comments</comments>
		<pubDate>Tue, 24 Sep 2019 08:56:35 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=29033</guid>
		<description><![CDATA[<p>简介 Go SDK自带了Profiling库，可以用来识别程序的缺陷、性能瓶颈。内置以下剖析能力： CPU剖析（profile）：报告CPU的使用情况，定位到热点（消耗CPU周期最多的）代码。默认情况下Go以100HZ的频率进行CPU采样 内存剖析（heap）：报告堆内存当前分配（存活对象）情况。默认情况下每分配512KB进行内存采样。你可以使用URL参数gc，提示报告前进行GC 内存剖析（allocs）：报告所有内存分配历史 线程创建（threadcreate）：报告导致新OS线程创建的代码片段，分析阻塞式系统调用 协程剖析（goroutine）：报告所有Goroutine的调用栈 阻塞剖析（block）：报告Goroutine在哪些同步原语（包括定时器通道）上阻塞，显示调用栈。必须显式调用[crayon-69d3148a64242187651946-i/]来启用此特性。默认每发生一次阻塞均采样 互斥量剖析（mutex）：报告锁竞争情况，显示持有互斥量的代码的调用栈。当你认为CPU英文锁竞争而没有被完全使用时，显式调用[crayon-69d3148a64246585604672-i/] 来启用此特性 执行追踪（trace）：追踪当前应用程序的执行栈 此外，Go还允许定制自己的剖析，在代码中手工报告性能分析数据。 数据采集 要采集一个Go应用的剖析数据，有两种方式： 利用runtime/pprof包，进行剖析数据采集，并且在应用退出时将剖析数据写入到文件 利用net/http/pprof包，进行剖析数据采集，支持连接到HTTP服务实时分析 不管使用那种方式，都需要增加一些代码。 采集CPU 进行CPU剖析，添加如下代码： [crayon-69d3148a64248833309021/] 采集内存 进行内存剖析，添加如下代码： <a class="read-more" href="https://blog.gmem.cc/go-program-profiling">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-program-profiling">Go应用性能剖析</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Go SDK自带了Profiling库，可以用来识别程序的缺陷、性能瓶颈。内置以下剖析能力：</p>
<ol>
<li>CPU剖析（profile）：报告CPU的使用情况，定位到热点（消耗CPU周期最多的）代码。默认情况下Go以100HZ的频率进行CPU采样</li>
<li>内存剖析（heap）：报告堆内存当前分配（存活对象）情况。默认情况下每分配512KB进行内存采样。你可以使用URL参数gc，提示报告前进行GC</li>
<li>内存剖析（allocs）：报告所有内存分配历史</li>
<li>线程创建（threadcreate）：报告导致新OS线程创建的代码片段，分析阻塞式系统调用</li>
<li>协程剖析（goroutine）：报告所有Goroutine的调用栈</li>
<li>阻塞剖析（block）：报告Goroutine在哪些同步原语（包括定时器通道）上阻塞，显示调用栈。必须显式调用<pre class="crayon-plain-tag">runtime.SetBlockProfileRate</pre>来启用此特性。默认每发生一次阻塞均采样</li>
<li>互斥量剖析（mutex）：报告锁竞争情况，显示持有互斥量的代码的调用栈。当你认为CPU英文锁竞争而没有被完全使用时，显式调用<pre class="crayon-plain-tag">runtime.SetMutexProfileFraction</pre> 来启用此特性</li>
<li>执行追踪（trace）：追踪当前应用程序的执行栈</li>
</ol>
<p>此外，Go还允许定制自己的剖析，在代码中手工报告性能分析数据。</p>
<div class="blog_h1"><span class="graybg">数据采集</span></div>
<p>要采集一个Go应用的剖析数据，有两种方式：</p>
<ol>
<li>利用runtime/pprof包，进行剖析数据采集，并且在应用退出时将剖析数据写入到文件</li>
<li>利用net/http/pprof包，进行剖析数据采集，支持连接到HTTP服务实时分析</li>
</ol>
<p>不管使用那种方式，都需要增加一些代码。</p>
<div class="blog_h2"><span class="graybg">采集CPU</span></div>
<p>进行CPU剖析，添加如下代码：</p>
<pre class="crayon-plain-tag">f, err := os.Create(*cpuprofile)
if err != nil {
    log.Fatal("could not create CPU profile: ", err)
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
    log.Fatal("could not start CPU profile: ", err)
}
// 停止采样，并将剖析概要信息记录到文件
// 此方法实际上会将采样率设置为0
defer pprof.StopCPUProfile()</pre>
<div class="blog_h2"><span class="graybg">采集内存</span></div>
<p>进行内存剖析，添加如下代码：</p>
<pre class="crayon-plain-tag">// 设置采样率，默认每分配512*1024字节采样一次。如果设置为0则禁止采样，只能设置一次
runtime.MemProfileRate = *memProfileRate


f, err := os.Create(*memprofile)
if err != nil {
    log.Fatal("could not create memory profile: ", err)
}
defer f.Close()
runtime.GC() // 执行GC，避免垃圾对象干扰
// 将剖析概要信息记录到文件
if err := pprof.WriteHeapProfile(f); err != nil {
    log.Fatal("could not write memory profile: ", err)
}</pre>
<div class="blog_h2"><span class="graybg">采集阻塞</span></div>
<p>调用下面的方法启用此功能：</p>
<pre class="crayon-plain-tag">runtime.SetBlockProfileRate(5)</pre>
<p>参数5表示，每发生5次Goroutine阻塞事件则采样一次。默认值1。 </p>
<p>下面的代码演示了如何将阻塞剖析概要信息记录到文件：</p>
<pre class="crayon-plain-tag">func stopBlockProfile() {
    if *blockProfile != "" &amp;&amp; *blockProfileRate &gt;= 0 {
        f, err := os.Create(*blockProfile)
        if err = pprof.Lookup("block").WriteTo(f, 0); err != nil {
            fmt.Fprintf(os.Stderr, "Can not write %s: %s", *blockProfile, err)
        }
        f.Close()
    }
} </pre>
<div class="blog_h2"><span class="graybg">采集互斥锁</span></div>
<p>从Go 1.8开始，支持采集处于竞态条件的互斥锁，调用下面的方法启用此功能：</p>
<pre class="crayon-plain-tag">runtime.SetMutexProfileFraction(5)</pre>
<p>此调用允许你捕获<span style="background-color: #c0c0c0;">处于竞态条件的Goroutine的调用栈的一部分</span>。</p>
<p>在进行测试时，不需要上述显式的调用，使用命令行参数即可：</p>
<pre class="crayon-plain-tag">go test -mutexprofile=mutex.out </pre>
<div class="blog_h2"><span class="graybg">通过HTTP暴露</span></div>
<p>包net/http/pprof能够将实时剖析数据通过HTTP暴露为pprof可视化工具能识别的格式。</p>
<p>要使用此包，需要导入：</p>
<pre class="crayon-plain-tag">import _ "net/http/pprof"</pre>
<p>你需要为pprof提供一个HTTP服务器：</p>
<pre class="crayon-plain-tag">go func() {
	log.Println(http.ListenAndServe("localhost:6060", nil))
}()</pre>
<p>如果你不使用<pre class="crayon-plain-tag">http.DefaultServeMux</pre>（如上代码），则需要手工注册路由规则：</p>
<pre class="crayon-plain-tag">r.HandleFunc("/debug/pprof/", pprof.Index)
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
r.HandleFunc("/debug/pprof/trace", pprof.Trace) </pre>
<div class="blog_h1"><span class="graybg">pprof</span></div>
<div class="blog_h2"><span class="graybg">读取剖析数据</span></div>
<div class="blog_h3"><span class="graybg">通过HTTP</span></div>
<p>要连接到HTTP服务进行实时分析，使用如下命令：</p>
<pre class="crayon-plain-tag"># 设置剖析摘要信息存放目录
export PPROF_TMPDIR=/tmp/pprof

# 获取堆快照
go tool pprof http://localhost:6060/debug/pprof/heap
# 获取从启动依赖的内存分配历史
go tool pprof http://localhost:6060/debug/pprof/allocs

# 30秒CPU分析，需要等待30秒才能看到命令提示符
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Goroutine阻塞分析
go tool pprof http://localhost:6060/debug/pprof/block

# 收集5秒的执行调用栈
wget http://localhost:6060/debug/pprof/trace?seconds=5

# 互斥锁分析
go tool pprof http://localhost:6060/debug/pprof/mutex</pre>
<p>要查看所有可用的剖析， 访问 http://localhost:6060/debug/pprof/ 。</p>
<div class="blog_h3"><span class="graybg">通过文件</span></div>
<pre class="crayon-plain-tag">#             可执行文件路径
#                      保存的剖析摘要文件
go tool pprof bin/Temp mutex.mprof</pre>
<div class="blog_h2"><span class="graybg">交互式命令</span></div>
<p>通过工具go tool pprof打开URL或文件后，会显示一个<pre class="crayon-plain-tag">(pprof)</pre>提示符，你可以使用以下命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 80px; text-align: center;">命令</td>
<td style="width: 20%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">gv</td>
<td>[focus]</td>
<td>
<p>将当前概要文件以图形化和层次化的形式显示出来。当没有任何参数时，在概要文件中的所有采样都会被显示</p>
<p>如果指定了focus参数，则只显示<span style="background-color: #c0c0c0;">调用栈中有名称与此参数相匹配的函数或方法的采样</span>。<pre class="crayon-plain-tag">focus</pre>参数应该是一个正则表达式</p>
<p>需要dot、gv命令，执行下面的命令安装：</p>
<p><pre class="crayon-plain-tag">sudo apt-get install graphviz
sudo apt-get install gv</pre>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>web</strong></td>
<td>[focus]</td>
<td>与gv命令类似，web命令也会用图形化的方式来显示概要文件。但不同的是，web命令是在一个Web浏览器中显示它</td>
</tr>
<tr>
<td class="blog_h3"><strong>list</strong></td>
<td>[routine_regexp]</td>
<td>列出名称与参数<pre class="crayon-plain-tag">routine_regexp</pre>代表的正则表达式相匹配的函数或方法的相关源代码</td>
</tr>
<tr>
<td class="blog_h3">weblist</td>
<td>[routine_regexp]</td>
<td> 在Web浏览器中显示与list命令的输出相同的内容。它与list命令相比的优势是，在我们点击某行源码时还可以显示相应的汇编代码 </td>
</tr>
<tr>
<td class="blog_h3"><strong>top[N]</strong></td>
<td>[--cum]</td>
<td>
<p>top命令可以以本地采样计数为顺序列出函数或方法及相关信息</p>
<p>如果存在标记<pre class="crayon-plain-tag">--cum</pre>则以累积采样计数为顺序</p>
<p>默认情况下top命令会列出前10项内容。但是如果在top命令后面紧跟一个数字，那么其列出的项数就会与这个数字相同</p>
</td>
</tr>
<tr>
<td>traces</td>
<td> </td>
<td>打印所有采集的样本</td>
</tr>
<tr>
<td class="blog_h3">disasm</td>
<td>[routine_regexp]</td>
<td>显示名称与参数<pre class="crayon-plain-tag">routine_regexp</pre>相匹配的函数或方法的反汇编代码。并且，在显示的内容中还会标注有相应的采样计数</td>
</tr>
<tr>
<td class="blog_h3">callgrind</td>
<td>[filename]</td>
<td>利用callgrind工具生成统计文件。在这个文件中，说明了程序中函数的调用情况。如果未指定<pre class="crayon-plain-tag">filename</pre>参数，则直接调用kcachegrind工具。kcachegrind可以以可视化的方式查看callgrind工具生成的统计文件</td>
</tr>
<tr>
<td class="blog_h3">help</td>
<td> </td>
<td>显示帮助</td>
</tr>
<tr>
<td class="blog_h3">quit</td>
<td> </td>
<td>退出 </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Web UI</span></div>
<p>调用pprof时，可以选择启动一个Web UI，指定-http选项即可：</p>
<pre class="crayon-plain-tag">go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap</pre>
<p> 你可以访问Web UI来查看烈焰图等高级图表。</p>
<div class="blog_h1"><span class="graybg">数据分析</span></div>
<div class="blog_h2"><span class="graybg">分析CPU</span></div>
<div class="blog_h3"><span class="graybg">测试代码</span></div>
<p>这里使用一段CPU密集型代码来学习CPU剖析：</p>
<pre class="crayon-plain-tag">package main

import (
	"net/http"
	_ "net/http/pprof"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()

	wg.Add(1)
	go calculte(wg)
	wg.Wait()
}

func calculte(wg sync.WaitGroup) {
	for i := 0; i &lt; 100000000; i++ {
		time.Sleep(time.Millisecond)
		cpuBound(i)
	}
	wg.Done()
}

func cpuBound(i int) {
	factorial(i)
	sieveOfEratosthenes(i)
}
func sieveOfEratosthenes(N int) (primes []int) {
	b := make([]bool, N)
	for i := 2; i &lt; N; i++ {
		if b[i] == true {
			continue
		}
		primes = append(primes, i)
		for k := i * i; k &lt; N; k += i {
			b[k] = true
		}
	}
	return
}

func factorial(x int) int {
	if x == 0 {
		return 1
	}

	return x * factorial(x-1)
}</pre>
<div class="blog_h3"><span class="graybg">top</span></div>
<pre class="crayon-plain-tag">(pprof) top10
Showing nodes accounting for 24630ms, 83.75% of 29410ms total
Dropped 107 nodes (cum &lt;= 147.05ms)
Showing top 10 nodes out of 31
      flat  flat%   sum%        cum   cum%
    4720ms 16.05% 16.05%     4990ms 16.97%  main.sieveOfEratosthenes
    4510ms 15.33% 31.38%    19440ms 66.10%  runtime.gentraceback
    3550ms 12.07% 43.45%    23490ms 79.87%  main.factorial
    2810ms  9.55% 53.01%     8460ms 28.77%  runtime.getStackMap
    2130ms  7.24% 60.25%     3220ms 10.95%  runtime.funcdata
    2070ms  7.04% 67.29%     2630ms  8.94%  runtime.findfunc
    1540ms  5.24% 72.53%     1740ms  5.92%  runtime.pcvalue
    1400ms  4.76% 77.29%     1400ms  4.76%  runtime.add
    1000ms  3.40% 80.69%     9610ms 32.68%  runtime.adjustframe
     900ms  3.06% 83.75%     1070ms  3.64%  runtime.pcdatastart
...
         0     0% 97.18%      5.01s 17.04%  main.calculte
         0     0% 97.18%      4.99s 16.97%  main.cpuBound</pre>
<p>使用top命令可以直接看到消耗CPU最多的方法，各列含义如下：</p>
<ol>
<li>flat：在采样期间，<span style="background-color: #c0c0c0;">此函数正在执行的次数 * 10ms</span>。 可以用来粗略估计函数的运行耗时，不包含当前函数调用其它函数并等待返回的时间</li>
<li>flat%：flat / 总采样时间。估算函数运行耗CPU占比</li>
<li>sum%：当前行加上前面所有行的flat%总和</li>
<li>cum：在采样期间，此函数出现在调用栈的次数*10ms。和flat相比，该指标包含子函数耗时</li>
<li>cum%：cum/总采样时间</li>
</ol>
<p>要以cum降序输出，执行<pre class="crayon-plain-tag">top10 -cum</pre>。</p>
<p>上面的例子中，factorial是自递归调用，其cum值不知道为何比父例程cpuBound还要大得多，不符合只觉。</p>
<div class="blog_h3"><span class="graybg">list</span></div>
<p>通过top定位到耗时函数后，可以进一步使用该命令，分析函数每一行代码消耗多少时间。</p>
<p>函数cpuBound调用两个子例程factorial、sieveOfEratosthenes，它们都是非常耗时的：</p>
<pre class="crayon-plain-tag">(pprof) list cpuBound
Total: 29.41s
ROUTINE ======================== main.cpuBound in /home/alex/Go/workspaces/default/src/git.gmem.cc/alex/go-study/golang/profile.go
         0      4.99s (flat, cum) 16.97% of Total
         .          .     27:	wg.Done()
         .          .     28:}
         .          .     29:
         .          .     30:func cpuBound(i int) {
         .          .     31:	factorial(i)
         .      4.99s     32:	sieveOfEratosthenes(i)
         .          .     33:}
         .          .     34:func sieveOfEratosthenes(N int) (primes []int) {
         .          .     35:	b := make([]bool, N)
         .          .     36:	for i := 2; i &lt; N; i++ {
         .          .     37:		if b[i] == true {</pre>
<p>从上面的输出可以看到， cpuBound的全部时间均消耗在对sieveOfEratosthenes的调用上，而factorial这个自递归调用的耗时无法体现。</p>
<div class="blog_h3"><span class="graybg">web </span></div>
<p>使用此命令可以生成一个SVG图片，清晰的显示调用关系图。图中越红的节点消耗CPU越多： </p>
<p><img class="aligncenter" style="width: 611px;" src="https://cdn.gmem.cc/wp-content/uploads/2019/09/pprof001.svg" alt="" height="879" /></p>
<p>从图中可以看到factorial函数引发的调用链最耗时，calculte其次。由于factorial是自递归调用，calculte到factorial的调用关系没有识别出来。</p>
<div class="blog_h2"><span class="graybg">分析内存</span></div>
<div class="blog_h3"><span class="graybg">测试代码</span></div>
<p>这里使用一段内存消耗型代码来学习CPU剖析：</p>
<pre class="crayon-plain-tag">package main

import (
	"net/http"
	_ "net/http/pprof"
	"time"
)

type pkg struct {
	blob
}
type blob struct {
	data [1024]int
}

func main() {
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()
	consume()
}

func consume() {
	for i := 0; i &lt; 100000000; i++ {
		time.Sleep(time.Millisecond * 1)
		b100 := createBlob(100)
		println(b100)
		p100 := createPkg(100)
		println(p100)
	}
}

func createPkg(i int) interface{} {
	ps := make([]pkg, 0)
	for x := 0; x &lt; i; x++ {
		ps = append(ps, pkg{})
	}
	return ps
}

func createBlob(i int) []blob {
	bs := make([]blob, 0)
	for x := 0; x &lt; i; x++ {
		bs = append(bs, blob{})
	}
	return bs
}</pre>
<div class="blog_h3"><span class="graybg">heap</span></div>
<p>执行下面的命令，可以获取一个堆快照，快照中包含所有存活对象：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/heap</pre>
<p>使用top命令可以看到哪些方法分配了最多的内存：</p>
<pre class="crayon-plain-tag">(pprof) top
Showing nodes accounting for 1.95MB, 100% of 1.95MB total
      flat  flat%   sum%        cum   cum%
    1.95MB   100%   100%     1.95MB   100%  main.createPkg
         0     0%   100%     1.95MB   100%  main.consume
         0     0%   100%     1.95MB   100%  main.main
         0     0%   100%     1.95MB   100%  runtime.main</pre>
<p>可以看到，在本次快照中，createPkg分配的内存最多。</p>
<p>使用list命令，可以进一步定位到createPkg的哪一行代码分配了这些内存：</p>
<pre class="crayon-plain-tag">(pprof) list createPkg
Total: 1.95MB
ROUTINE ======================== main.createPkg in /home/alex/Go/workspaces/default/src/git.gmem.cc/alex/go-study/golang/heap.go
    1.95MB     1.95MB (flat, cum)   100% of Total
         .          .     31:}
         .          .     32:
         .          .     33:func createPkg(i int) interface{} {
         .          .     34:   ps := make([]pkg, 0)
         .          .     35:   for x := 0; x &lt; i; x++ {
    1.95MB     1.95MB     36:           ps = append(ps, pkg{})
         .          .     37:   }
         .          .     38:   return ps
         .          .     39:}
         .          .     40:
         .          .     41:func createBlob(i int) []blob {</pre>
<p>可以看到，全部内存均由于代码<pre class="crayon-plain-tag">ps = append(ps, pkg{})</pre>分配。 </p>
<p>类似的，使用web命令可以展示出内存分配的调用栈。</p>
<div class="blog_h3"><span class="graybg">allocs</span></div>
<p>执行下面的命令，可以获取程序运行依赖，所有内存分配的历史：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/allocs</pre>
<p>用top命令看，分配内存的量明显比heap剖析大的多：</p>
<pre class="crayon-plain-tag">(pprof) top
Showing nodes accounting for 5.69TB, 100% of 5.69TB total
Dropped 33 nodes (cum &lt;= 0.03TB)
      flat  flat%   sum%        cum   cum%
    2.85TB 50.00% 50.00%     2.85TB 50.00%  main.createPkg
    2.85TB 50.00%   100%     2.85TB 50.00%  main.createBlob
         0     0%   100%     5.69TB   100%  main.consume
         0     0%   100%     5.69TB   100%  main.main
         0     0%   100%     5.69TB   100%  runtime.main</pre>
<p>使用web命令，可以展示出内存分配的调用栈：</p>
<p><img class="aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2019/09/pprof002.svg" alt="" width="694" height="266" /></p>
<div class="blog_h3"><span class="graybg">内存泄漏</span></div>
<p>所谓内存泄漏，意味着占用的内存一直无法释放。在pprof中，泄漏的内存一直存在于heap剖析的输出中，并且随着运行时间的增加，迟早会出现在top命令的输出中。</p>
<div class="blog_h2"><span class="graybg">分析阻塞</span></div>
<div class="blog_h3"><span class="graybg">测试代码</span></div>
<p>这里使用一段内存消耗型代码来学习阻塞剖析：</p>
<pre class="crayon-plain-tag">package main

import (
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"sync"
	"time"
)

var mutex sync.Mutex

func main() {
	// rate = 1：统计所有的 block event,
	// rate &lt;=0：关闭block profiling
	// rate &gt; 1：阻塞时间t&gt;rate那秒的event 一定会被统计，小于rate则有t/rate 的几率被统计
	runtime.SetBlockProfileRate(1 * 1000 * 1000)
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()
	var wg sync.WaitGroup
	for ; ; {
		wg.Add(1)
		mutex.Lock()
		go worker(&amp;wg)
		time.Sleep(2 * time.Millisecond)
		mutex.Unlock()
		wg.Wait()
	}
}
func worker(wg *sync.WaitGroup) {
	defer wg.Done()
	mutex.Lock()
	time.Sleep(1 * time.Millisecond)
	mutex.Unlock()
}</pre>
<div class="blog_h3"><span class="graybg">top</span></div>
<p>执行下面的命令，可以获取所有录制的阻塞事件：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/block</pre>
<p>top显示的是阻塞时间最长的方法：</p>
<pre class="crayon-plain-tag">(pprof) top
Showing nodes accounting for 2.99mins, 100% of 2.99mins total
Dropped 9 nodes (cum &lt;= 0.01mins)
      flat  flat%   sum%        cum   cum%
  1.95mins 65.37% 65.37%   1.95mins 65.37%  sync.(*Mutex).Lock
  1.03mins 34.63%   100%   1.03mins 34.63%  sync.(*WaitGroup).Wait
         0     0%   100%   1.03mins 34.63%  main.main
         0     0%   100%   1.95mins 65.37%  main.worker
         0     0%   100%   1.03mins 34.63%  runtime.main
(pprof)</pre>
<p>这里可以看到阻塞时间都消耗在互斥量的Lock和等待组的Wait方法上。 </p>
<div class="blog_h3"><span class="graybg">web </span></div>
<p>使用top无法感知什么代码导致了阻塞，你可以使用web，展示导致阻塞的调用栈。 </p>
<div class="blog_h2"><span class="graybg">分析互斥锁</span></div>
<div class="blog_h3"><span class="graybg">测试代码</span></div>
<pre class="crayon-plain-tag">package main

import (
	"math/rand"
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"sync"
	"time"
)

var mutex sync.Mutex

func main() {
	// rate = 0：关闭 mutex prof
	// rate = 1：记录所有的 mutex event
	// rate &gt; 1：随机记录 1/rate 的 mutex event
	runtime.SetMutexProfileFraction(1)
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()

	go worker()
	go worker()
	var wg sync.WaitGroup
	wg.Add(1)
	wg.Wait()
}
func worker() {
	for ; ; {
		mutex.Lock()
		time.Sleep(time.Duration(rand.New(rand.NewSource(time.Now().Unix())).Intn(10)) * time.Second)
		mutex.Unlock()
	}
}</pre>
<div class="blog_h3"><span class="graybg">top</span></div>
<p>执行下面的命令，可以获取所有录制的互斥锁事件：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/mutex</pre>
<p>使用top命令，可以看到锁竞争的位置。</p>
<div class="blog_h2"><span class="graybg">分析Goroutine</span></div>
<p>可以执行：</p>
<pre class="crayon-plain-tag">curl -s  http://localhost:6060/debug/pprof/goroutine?debug=2</pre>
<p>来获取所有Goroutine的状态、调用栈。</p>
<p>如果Goroutine因为读写通道而阻塞，可以看到类似下面的输出：</p>
<pre class="crayon-plain-tag">goroutine 1 [chan receive, 7 minutes]:
main.main()
	/home/alex/Go/workspaces/default/src/git.gmem.cc/alex/go-study/golang/goroutine.go:21 +0x71</pre>
<p>在这个例子中，主协程已经等待达7分钟。这种在通道上的等待是无法通过阻塞分析看到的。</p>
<p>如果Goroutine正在等待（包括网络）IO完成，可以看到类似下面的输出：</p>
<pre class="crayon-plain-tag">goroutine 436 [IO wait]:
internal/poll.runtime_pollWait(0x7ff9056e2dd8, 0x72, 0xb)</pre>
<div class="blog_h1"><span class="graybg">定制剖析</span></div>
<p>Go允许开发人员扩展自己的Profile，来跟踪任何资源的创建/释放。</p>
<p>假设你负责编写某个Blob服务器的客户端库，用户的需求是随时了解某个客户端实例打开了多少Blob。 你可以使用定制剖析满足此需求：</p>
<pre class="crayon-plain-tag">package blobstore

import "runtime/pprof"

// 定制剖析
var openBlobProfile = pprof.NewProfile("blobstore.Open")

// 打开一个Blob，所有Blob不再使用之后需要关闭
func Open(name string) (*Blob, error) {
	blob := &amp;Blob{name: name}

	// ... 在这里加载并初始化Blob


	// 此方法将当前调用栈加入到剖析中，并且将此栈关联到对象blob
	// 信息存放在内部的一个map中，以blob为key，这意味着：
	//   1.blob必须适合用作key，而且它
	//   2.在显示调用Remove之前不会被GC
	// 如果剖析已经包含blob的调用栈，则panic

	// 2 表示跳过的栈帧数量，对于调用栈
	//   Add
	//   called from rpc.NewClient
	//   called from mypkg.Run
	//   called from main.main
	//  skip=0 从rpc.NewClient中的Add调用处开始记录
	//  skip=1 从mypkg.Run的NewClient调用处开始记录 
	openBlobProfile.Add(blob, 2)
	return blob, nil
}

// 关闭Blob并释放底层资源
func (b *Blob) Close() error {
	// 从Profile中移除对象b关联的调用栈
	openBlobProfile.Remove(b)
	return nil
}</pre>
<p>如果此客户端库的使用者，想知道自己的程序当前打开了多少Blob，在什么地方（代码位置）打开的，可以这样编写：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"math/rand"
	"net/http"
	_ "net/http/pprof"
	"time"

	"myproject.org/blobstore"
)

func main() {
	for i := 0; i &lt; 1000; i++ {
		name := fmt.Sprintf("task-blob-%d", i)
		go func() {
			// 打开Blob，会导致剖析记录数据
			b, err := blobstore.Open(name)
			if err != nil {
			}
			defer b.Close()
		}()
	}
	http.ListenAndServe("localhost:6060", nil)
}</pre>
<p>程序运行期间，使用如下命令即可看到打开了哪些Blob：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/blobstore.Open

(pprof) top
Showing nodes accounting for 800, 100% of 800 total
      flat  flat%   sum%        cum   cum%
       800   100%   100%        800   100%  main.main.func1 /Users/jbd/src/hello/main.go</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-program-profiling">Go应用性能剖析</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/go-program-profiling/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Kubernetes故障检测和自愈</title>
		<link>https://blog.gmem.cc/problem-detection-and-auto-repairing-in-k8s</link>
		<comments>https://blog.gmem.cc/problem-detection-and-auto-repairing-in-k8s#comments</comments>
		<pubDate>Mon, 16 Sep 2019 09:00:57 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=28699</guid>
		<description><![CDATA[<p>前言 在Kubernetes日常运维过程中，会出现各种各样的问题，例如： 节点CNI不可用，其它节点无法连接到故障节点的Pod Subpath方式挂载的Configmap，特定条件下出现Pod无限重启的问题 集群DNS服务器无法通过上游DNS解析外部名称 节点假死，但是持有的Ceph RBD的Watcher不释放，导致有状态服务的Pod调度走后仍然无法启动 误删Etcd数据、持久卷 这些问题导致部分Pod、节点、甚至整个集群不可用，需要人工运维才能恢复。 从接收到告警到运维人员手工处理完毕，可能已经过了1小时，严重影响服务质量。但是如果能识别这些告警并将运维知识转化为代码，某些问题可能在一分钟内就被发现和解决。 本文调研商业产品和社区的集群/节点故障检测、修复技术的现状，为自研节点自愈产品提供参考。 故障类型 节点故障 Pod所在节点的内核、CRI运行时等出现问题，无法支持Pod的运行。针对这类故障，社区或商业的解决方案较多，例如社区的NPD项目、GKE的节点修复功能。 组件故障 组件故障可以认为是节点故障的子类，只是故障来源是K8S基础组件的一部分。 K8S集群基础组件出现故障，可能导致集群或在节点的部分功能不可用。我在线上环境遇到过的故障包括： KubeDNS故障：6个DNS Pod中的2个出现无法解析外部DNS名称的情况。后果是大量线上业务因域名解析 Calico CNI故障：少数几个节点的容器网络和外部断开，节点访问自身的Pod IP没有问题，但是其它节点无法访问故障节点的Pod <a class="read-more" href="https://blog.gmem.cc/problem-detection-and-auto-repairing-in-k8s">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/problem-detection-and-auto-repairing-in-k8s">Kubernetes故障检测和自愈</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>在Kubernetes日常运维过程中，会出现各种各样的问题，例如：</p>
<ol>
<li>节点CNI不可用，其它节点无法连接到故障节点的Pod</li>
<li>Subpath方式挂载的Configmap，特定条件下出现Pod无限重启的问题</li>
<li>集群DNS服务器无法通过上游DNS解析外部名称</li>
<li>节点假死，但是持有的Ceph RBD的Watcher不释放，导致有状态服务的Pod调度走后仍然无法启动</li>
<li>误删Etcd数据、持久卷</li>
</ol>
<p>这些问题导致部分Pod、节点、甚至整个集群不可用，需要人工运维才能恢复。</p>
<p>从接收到告警到运维人员手工处理完毕，可能已经过了1小时，严重影响服务质量。但是如果能识别这些告警并<span style="background-color: #c0c0c0;">将运维知识转化为代码，某些问题可能在一分钟内就被发现和解决</span>。</p>
<p>本文调研商业产品和社区的集群/节点故障检测、修复技术的现状，为自研节点自愈产品提供参考。</p>
<div class="blog_h2"><span class="graybg">故障类型</span></div>
<div class="blog_h3"><span class="graybg">节点故障</span></div>
<p>Pod所在节点的内核、CRI运行时等出现问题，无法支持Pod的运行。针对这类故障，社区或商业的解决方案较多，例如社区的NPD项目、GKE的节点修复功能。</p>
<div class="blog_h3"><span class="graybg">组件故障</span></div>
<p>组件故障可以认为是节点故障的子类，只是故障来源是K8S基础组件的一部分。</p>
<p>K8S集群基础组件出现故障，可能导致集群或在节点的部分功能不可用。我在线上环境遇到过的故障包括：</p>
<ol>
<li>KubeDNS故障：6个DNS Pod中的2个出现无法解析外部DNS名称的情况。后果是大量线上业务因域名解析</li>
<li>Calico CNI故障：少数几个节点的容器网络和外部断开，节点访问自身的Pod IP没有问题，但是其它节点无法访问故障节点的Pod IP。这种情况下，<span style="background-color: #c0c0c0;">Pod本机的健康检查无效</span>，导致故障实例持续存在，一定比例的业务请求失败</li>
</ol>
<p>由于K8S生态主要依赖于开源社区，很多组件不成熟，存在缺陷，因此这类故障较为纷杂。社区没有发现知名的解决方案，主要依赖于日常运维中知识的积累，而且这些运维知识往往是环境相关、K8S版本或组件版本相关的，通用性较差。</p>
<div class="blog_h3"><span class="graybg">集群故障</span></div>
<p>K8S控制平面不存在单点问题 ，通常情况下，出现整个集群的故障的概率是很低的。但是，某些行业对数据安全和可用性要求极高，另外，也出现过误操作导致集群破坏的案例，集群故障恢复还是需要考虑的。</p>
<p>应对集群故障的主要手段就是备份和恢复，Velero可以帮助我们实现这一点。它支持K8S资源（Etcd）、持久卷的备份，通过开发插件，某些存储后端可以基于快照来备份，效率很高。</p>
<div class="blog_h2"><span class="graybg">故障检测</span></div>
<p>我认为节点/组件的故障，根据需要，可以从两个角度发起。</p>
<div class="blog_h3"><span class="graybg">本地检测</span></div>
<p>绝大部分故障检测，在节点本地进行就足够了，这样做效率高，避免不必要的网络流量。</p>
<div class="blog_h3"><span class="graybg">远程检测</span></div>
<p>少部分网络相关的故障，可能从节点无法检测，这就需要远程检测。</p>
<p>还是上面的Calico CNI故障的例子，故障节点本身访问自己的Pod IP是畅通的，然而外部却无法访问。我们的应对方案，是运行在集群中运行3个Health Check Controller，如果大部分副本判定：节点A网络畅通但是节点A上的Pod Ip却无法联通，则认定节点A的CNI出现故障，需要进行处理。</p>
<div class="blog_h1"><span class="graybg">商业产品调研</span></div>
<div class="blog_h2"><span class="graybg">GKE</span></div>
<p>谷歌K8S引擎（Google Kubernetes Engine）提供了自动修复节点的功能。GKE会周期性的检测集群中每个节点的健康状态，如果某个节点的健康检查连续N次失败，则启动一个修复进程，对节点进行修复。</p>
<p>处于Ready状态的节点被认为是健康的，不健康节点可能处于以下状况：</p>
<ol>
<li>连续数次健康检查，报告NotReady状态</li>
<li>在指定的时间范围内，节点没有报告任何状态</li>
<li>节点在一个指定的时间范围内，处于磁盘空间不足的状态</li>
</ol>
<p>GKE修复节点的方法比较简单，就是Drain并重新创建。Drain操作会导致节点上的Pod被驱除。</p>
<p>如果多个节点需要修复，GKE可以并行的执行修复。</p>
<div class="blog_h1"><span class="graybg">node-problem-detector</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>这是一个K8S加载项（Addon），目的是将节点故障暴露给集群管理的上层组件。NPD通常运行为DaemonSet，也可以作为独立进程运行。NPD会检测各种各样的节点问题，例如：</p>
<ol>
<li>基础设施服务故障：例如NTP服务宕机</li>
<li>硬件问题：CPU、内存、磁盘故障</li>
<li>内核问题：内核死锁、文件系统损坏</li>
<li>容器运行时错误：Docker守护进程假死</li>
</ol>
<p>并报告给APIServer，报告的主要方式包括：</p>
<ol>
<li>NodeCondition：当遇到永久性的节点故障，导致其不可用时，设置节点的NodeCondition</li>
<li>Event：可能对Pod产生影响的临时信息</li>
</ol>
<p>在没有引入NPD的情况下，上面的各种节点问题对于K8S集群管理上层组件不可见，因此K8S会继续向问题节点调度Pod。</p>
<div class="blog_h2"><span class="graybg">PDS（Monitors）</span></div>
<p>Problem Daemon（在代码内部也叫Monitor）是NPD的子守护进程，每个PD监控一个特定类型的节点故障，并报告给NPD。目前<span style="background-color: #c0c0c0;">PD以Goroutine的形式运行</span>在NPD中，未来会支持在独立进程（容器）中运行并编排为一个Pod。在编译期间，可以通过相应的标记<span style="background-color: #c0c0c0;">禁用每一类PD</span>。</p>
<p>目前可用的PD包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 160px; text-align: center;">PD</td>
<td style="width: 130px; text-align: center;">NodeCondition</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">KernelMonitor</td>
<td>KernelDeadlock</td>
<td>
<p>监控内核日志，根据预定义规则来报告问题、指标</p>
<p>使用标记禁用：disable_system_log_monitor</p>
<p>KernelMonitor、AbrtAdaptor都属于System Log Monitor，只是使用的配置文件不同。</p>
<p>SLM支持基于文件的日志、Journald、kmsg。要监控其它日志，需要实现LogWatcher接口</p>
<p> 对于临时问题，SLM暴露counter类的指标，示例：</p>
<pre class="crayon-plain-tag"># HELP problem_counter Number of times a specific type of problem have occurred.
# TYPE problem_counter counter
problem_counter{reason="TaskHung"} 2</pre>
<p>对于永久问题，同时报告为gauge、counter：</p>
<pre class="crayon-plain-tag"># HELP problem_gauge Whether a specific type of problem is affecting the node or not.
# TYPE problem_gauge gauge
problem_gauge{condition="KernelDeadlock",reason="DockerHung"} 1</pre>
</td>
</tr>
<tr>
<td class="blog_h3">AbrtAdaptor</td>
<td>无</td>
<td>
<p>监控ABRT（Automatic Bug Report Tool）日志并报告。ABRT是一个健康监控守护进程，能够捕获内核问题、各种原因导致的应用崩溃
<p>使用标记禁用：disable_system_log_monitor</p>
</td>
</tr>
<tr>
<td class="blog_h3">CustomPluginMonitor</td>
<td>依用户配置</td>
<td>
<p>通过调用用户配置的脚本来检测各种节点问题</p>
<p>脚本退出码：</p>
<ol>
<li>0：对于Evnet来说表示Normal，对于NodeCondition表示False</li>
<li>1：对于Evnet来说表示Warning，对于NodeCondition表示True</li>
</ol>
<p>脚本输出应该小于80字节，避免给Etcd的存储造成压力</p>
<p>使用标记禁用：disable_custom_plugin_monitor</p>
</td>
</tr>
<tr>
<td class="blog_h3">SystemStatsMonitor</td>
<td>暂无</td>
<td>
<p>将各种健康相关的统计信息报告为Metrics</p>
<p>目前支持的组件仅仅有主机信息、磁盘：</p>
<p style="padding-left: 30px;">disk/io_time 设备队列非空时间，毫秒<br />disk/weighted_io 设备队列非空时间加权，毫秒<br />disk/avg_queue_len 上次调用插件以来，平均排队请求数</p>
<p>使用标记禁用：disable_system_stats_monitor</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Exporters</span></div>
<p>NPD提供了若干Exporter组件，能够将节点问题、指标报告给后端：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">Exporter</td>
<td style="text-align: center;"> </td>
</tr>
</thead>
<tbody>
<tr>
<td>Kubernetes Exporter</td>
<td>暴露临时问题为Event、永久问题为NodeCondition</td>
</tr>
<tr>
<td>Prometheus Exporter</td>
<td>暴露节点问题、指标为Prometheus metrics</td>
</tr>
<tr>
<td>Stackdriver Exporter</td>
<td>
<p>暴露节点问题、指标给Stackdriver监控API</p>
<p>使用标记禁用：disable_stackdriver_exporter</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<div class="blog_h3"><span class="graybg">节点监控</span></div>
<p>故障节点上的事件，会记录在宿主机的某些日志中。这些日志（例如内核日志）中噪音信息太多，NPD会提取其中有价值的信息，记录到自己的Pod日志中。你可以通过EFK收集这些信息，NPD也可以将这些信息报送给Prometheus。</p>
<div class="blog_h3"><span class="graybg">节点自愈</span></div>
<p>基于NPD的的节点自愈流程如下：</p>
<ol>
<li>NPD为故障节点添加额外的Condition元数据</li>
<li>Cordon并Drain故障节点</li>
<li>利用<a href="https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler">cluster-autoscaler</a>进行集群扩容，补充节点</li>
</ol>
<p>这个流程本质上是替换，而不是治愈节点。在裸金属K8S集群中，由于缺乏基础设施的支撑，自动扩充节点可能无法实现，只能通过更加精细的自动化运维，治愈节点的异常状态。</p>
<p>以CNI故障为例，可能的治愈流程如下：</p>
<ol>
<li>查询运维知识库，如果找到匹配项，执行对应的运维动作</li>
<li>如果上述步骤无效，尝试删除节点上负责CNI的Pod，以重置节点的路由、Iptables配置</li>
<li>如果上述步骤无效，尝试重启容器运行时</li>
<li>告警，要求运维人员介入</li>
</ol>
<div class="blog_h2"><span class="graybg">构建</span></div>
<p>NPD使用Go modules管理依赖，因此构建它需要Go SDK 1.11+：</p>
<pre class="crayon-plain-tag">cd $GOPATH/src/k8s.io
go get k8s.io/node-problem-detector
cd node-problem-detector

export GO111MODULE=on 
go mod vendor

# 设置构建标记
export BUILD_TAGS="disable_custom_plugin_monitor disable_system_stats_monitor"

# 在Ubuntu 14.04上需要安装
sudo apt install libsystemd-journal-dev
make all</pre>
<div class="blog_h2"><span class="graybg">用法</span></div>
<div class="blog_h3"><span class="graybg">安装</span></div>
<p>可以通过Helm安装：</p>
<pre class="crayon-plain-tag">helm install stable/node-problem-detector</pre>
<p>依据宿主机操作系统的不同，你可能需要修改挂载为HostPath的宿主机日志目录（默认<span class="pl-s">/var/log/）、内核消息目录（默认/dev/kmsg）的路径。</span> </p>
<p> 各PD的配置，均放在ConfigMap中，按需修改。</p>
<div class="blog_h3"><span class="graybg">命令行参数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 220px; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--hostname-override</td>
<td>覆盖NPD更新Condition、Evnet时使用的主机节点名，如果不指定，依次尝试NODE_NAME环境变量、os.Hostname</td>
</tr>
<tr>
<td>--config.system-log-monitor</td>
<td>PD AbrtAdaptor的配置文件路径，逗号分隔，示例：<br />
<pre class="crayon-plain-tag">{
	"plugin": "kmsg",
    // 读取内核环缓冲设备
	"logPath": "/dev/kmsg",
	"lookback": "5m",
	"bufferSize": 10,
	"source": "kernel-monitor",
	"metricsReporting": true,
    // 定义新的NodeConditions
	"conditions": [
		{
			"type": "KernelDeadlock",
			"reason": "KernelHasNoDeadlock",
			"message": "kernel has no deadlock"
		},
		{
			"type": "ReadonlyFilesystem",
			"reason": "FilesystemIsNotReadOnly",
			"message": "Filesystem is not read-only"
		}
	],
    // 检测问题的规则列表
	"rules": [
		{
            // 问题类别可以是temporary、permanent
			"type": "temporary",
			"reason": "OOMKilling",
            // 匹配日志内容，支持多行匹配
			"pattern": "Kill process \\d+ (.+) score \\d+ or sacrifice child\\nKilled process \\d+ (.+) total-vm:\\d+kB, anon-rss:\\d+kB, file-rss:\\d+kB.*"
		},
		{
			"type": "temporary",
			"reason": "TaskHung",
			"pattern": "task \\S+:\\w+ blocked for more than \\w+ seconds\\."
		},
		{
			"type": "temporary",
			"reason": "UnregisterNetDevice",
			"pattern": "unregister_netdevice: waiting for \\w+ to become free. Usage count = \\d+"
		},
		{
			"type": "temporary",
			"reason": "KernelOops",
			"pattern": "BUG: unable to handle kernel NULL pointer dereference at .*"
		},
		{
			"type": "temporary",
			"reason": "KernelOops",
			"pattern": "divide error: 0000 \\[#\\d+\\] SMP"
		},
		{
			// 永久问题，记录为Node对象的Conditions
			"type": "permanent",
			// NodeCondition.Type，驼峰式大小写
			"condition": "KernelDeadlock",
			// NodeCondition.Reason 同样驼峰式大小写，通常为上述Type的子类型
			"reason": "AUFSUmountHung",
			"pattern": "task umount\\.aufs:\\w+ blocked for more than \\w+ seconds\\."
		},
		{
			"type": "permanent",
			"condition": "KernelDeadlock",
			"reason": "DockerHung",
			"pattern": "task docker:\\w+ blocked for more than \\w+ seconds\\."
		},
		{
			"type": "permanent",
			"condition": "ReadonlyFilesystem",
			"reason": "FilesystemIsReadOnly",
			"pattern": "Remounting filesystem read-only"
		}
	]
}</pre></p>
<p>对于每个配置，NPD会启动一个独立的日志监控线程 </p>
</td>
</tr>
<tr>
<td>--config.system-stats-monitor</td>
<td>PD SystemStatsMonitor的配置文件路径，逗号分隔，示例：<br />
<pre class="crayon-plain-tag">{
	"disk": {
        // 指定需要收集的指标
		"metricsConfigs": {
			"disk/io_time": {
				"displayName": "disk/io_time"
			},
			"disk/weighted_io": {
				"displayName": "disk/weighted_io"
			},
			"disk/avg_queue_len": {
				"displayName": "disk/avg_queue_len"
			}
		},
        // 设置为true则将所有块设备（slave、holder除外）加入到指标收集列表
		"includeRootBlk": true,
        // 设置为true，则将所有分区加入到指标收集列表
		"includeAllAttachedBlk": true,
        // 此PD通过lsblk获取设备信息，此选项设置获取的超时
		"lsblkTimeout": "5s"
	},
	"host": {
		"metricsConfigs": {
			"host/uptime": {
				"displayName": "host/uptime"
			}
		}
	},
	"invokeInterval": "60s"
}</pre></p>
<p> 对于每个配置，NPD会启动一个独立的统计信息监控线程 </p>
</td>
</tr>
<tr>
<td>--config.custom-plugin-monitor</td>
<td>PD CustomPluginMonitor的配置文件路径，逗号分隔，示例：<br />
<pre class="crayon-plain-tag">{
  "plugin": "custom",
  "pluginConfig": {
    // 调用自定义插件的间隔
    "invoke_interval": "30s",
    // 调用自定义插件的超时，超过5s脚本没有退出则认为超时
    "timeout": "5s",
    // 最多读取自定义插件的标准输出的长度，用作Condition状态消息
    "max_output_length": 80,
    // 工作线程数量，也就是说多少个自定义插件可以被并发的调用
    "concurrency": 3,
    // 状态消息变更，是否应该导致Condition更新
    "enable_message_change_based_condition_update": false
  },
  // 其它字段和SLM类似
  "source": "ntp-custom-plugin-monitor",
  "metricsReporting": true,
  "conditions": [
    {
      "type": "NTPProblem",
      "reason": "NTPIsUp",
      "message": "ntp service is up"
    }
  ],
  "rules": [
    {
      "type": "temporary",
      "reason": "NTPIsDown",
      "path": "./config/plugin/check_ntp.sh",
      "timeout": "3s"
    },
    {
      "type": "permanent",
      "condition": "NTPProblem",
      "reason": "NTPIsDown",
      "path": "./config/plugin/check_ntp.sh",
      "timeout": "3s"
    }
  ]
}</pre></p>
<p> 插件的逻辑编写在脚本中：</p>
<pre class="crayon-plain-tag">#!/bin/bash

# NOTE: THIS NTP SERVICE CHECK SCRIPT ASSUME THAT NTP SERVICE IS RUNNING UNDER SYSTEMD.
#       THIS IS JUST AN EXAMPLE. YOU CAN WRITE YOUR OWN NODE PROBLEM PLUGIN ON DEMAND.

systemctl status ntp.service | grep 'Active:' | grep -q 'running'
ret=$?
if [ $ret -ne 0 ]; then
    echo "NTP service is down."
    exit 1
fi

echo "NTP service is up."
exit 0</pre>
<p>对于每个配置，NPD会启动一个独立的监控线程</p>
</td>
</tr>
<tr>
<td>--enable-k8s-exporter </td>
<td>启用Kubernetes Exporter，默认true </td>
</tr>
<tr>
<td>--apiserver-override</td>
<td>
<p>覆盖报告到的API Server的地址，格式和<a href="https://github.com/kubernetes/heapster/blob/master/docs/source-configuration.md#kubernetes">Heapster</a>的source标记相同</p>
<p>如果以Standalone方式运行NPD，需要设置inClusterConfig为false：</p>
<pre class="crayon-plain-tag">http://APISERVER_IP:APISERVER_PORT?inClusterConfig=false</pre>
</td>
</tr>
<tr>
<td>--address </td>
<td>NPD服务器的绑定地址</td>
</tr>
<tr>
<td>--port</td>
<td>NPD服务器的绑定端口，设置为0禁用</td>
</tr>
<tr>
<td>
<p>--prometheus-address<br />--prometheus-port
</td>
<td>Prometheus Export的监听地址，默认127.0.0.1:20257</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">治愈系统</span></div>
<p>在NPD的术语中，治愈系统（Remedy System）是一个或一组进程，负责分析NPD检测出的问题，并且采取补救措施，让K8S集群恢复健康状态。</p>
<p>目前官方提及的治愈系统有只有Draino。NPD项目并没有提供对Draino的集成，你需要手工部署和配置Draino。</p>
<div class="blog_h3"><span class="graybg">Draino</span></div>
<p><a href="https://github.com/planetlabs/draino">Draino</a>是Planet开源的小项目，最初在Planet用于解决GCE上运行的K8S集群的持久卷相关进程（mkfs.ext4、mount等）永久卡死在不可中断睡眠状态的问题。Draino的工作方式简单粗暴，只是检测到NodeCondition并Cordon、Drain节点。</p>
<p>基于Label和NodeCondition自动的Drain掉故障K8S节点：</p>
<ol>
<li>具有匹配标签的的K8S节点，只要进入指定的NodeCondition之一，立即禁止调度（Cordoned） </li>
<li>在禁止调度之后一段时间，节点被Drain掉</li>
</ol>
<p>Draino可以联用Cluster Autoscaler，自动的终结掉Drained的节点。</p>
<p>在Descheduler项目成熟以后，可以代替Draino。</p>
<div class="blog_h2"><span class="graybg">核心源码分析</span></div>
<div class="blog_h3"><span class="graybg">如何调试</span></div>
<p>建议本地启动NPD并调试，参考下面的命令行参数：</p>
<pre class="crayon-plain-tag">--hostname-override=xenial-100 --apiserver-wait-timeout=10s --logtostderr --stderrthreshold=0 -v=10 \
--apiserver-override=http://k8s.gmem.cc:6444?inClusterConfig=false \
--config.system-log-monitor=/home/alex/Go/workspaces/default/src/k8s.io/node-problem-detector/config/docker-monitor-filelog.json </pre>
<div class="blog_h3"><span class="graybg">核心模型</span></div>
<p>NPD抽象了一系列基本的类型，目前供内部使用，这些类型比起K8S API中的对应物更加轻量：</p>
<pre class="crayon-plain-tag">package types

import (
	"time"
	"github.com/spf13/pflag"
)

// 问题的严重性，目前仅仅支持Info和Warn，和K8S事件类型对应
type Severity string
const (
	Info Severity = "info"   // 对应K8S的Normal事件
	Warn Severity = "warn"   // 对应K8S的Warning事件
)

// NodeCondition的状态
type ConditionStatus string
const (
	// 节点处于目标状态
	True ConditionStatus = "True"
	// 节点不处于目标状态
	False ConditionStatus = "False"
	// 不清楚
	Unknown ConditionStatus = "Unknown"
)

// 建模NodeCondition
type Condition struct {
	// NodeCondition类型，例如KernelDeadlock, OutOfResource
	Type string `json:"type"`
	// 节点是否处于此NodeCondition
	Status ConditionStatus `json:"status"`
	// 节点转换为此Condition的时间
	Transition time.Time `json:"transition"`
	// 记录为何进入此Condition的简短原因
	Reason string `json:"reason"`
	// 人类可读的，进入此Condition的原因
	Message string `json:"message"`
}

// 建模Event
type Event struct {
	// 严重性
	Severity Severity `json:"severity"`
	// 事件发生时间
	Timestamp time.Time `json:"timestamp"`
	// 事件的简短原因
	Reason string `json:"reason"`
	// 人类可读的消息
	Message string `json:"message"`
}

// PD向NPD核心报告时使用的DTO
type Status struct {
	// PD的名称
	Source string `json:"source"`
	// 临时的节点问题 —— 事件对象，如果此Status用于Condition更新则此字段可以为空
	// 从老到新排列在数组中
	Events []Event `json:"events"`
	// 永久的节点问题 —— NodeCondition。PD必须总是在此字段报告最新的Condition
	Conditions []Condition `json:"conditions"`
}

// 建模问题分类
type Type string
const (
	// 临时问题，报告为Event
	Temp Type = "temporary"
	// 永久问题，报告为NodeCondition
	Perm Type = "permanent"
)

// PD的接口。PD根据配置的规则，监控并报告节点问题、收集Metrics
type Monitor interface {
	// 启动此PD，将返回一个通道，NPD核心从此通道获取状态更新
	// 如果此PD仅仅报告Metrics，不关注Problem，则返回的通道应该设置为nil
	Start() (&lt;-chan *Status, error)
	// 停止此PD
	Stop()
}

// 将节点的监控状态报告给某种控制平面，例如K8S API Server，或者Prometheus
type Exporter interface {
	// 报告问题，此方法由NPD核心调用，传递问题给Exporter，Exporter则将问题传递到NPD外部
	ExportProblems(*Status)
}

// PD类型，每个PD类型可以启动多个实例，每个实例对应一个配置
type ProblemDaemonType string

// 此映射建模所有PD的所有配置
// 1) 每个键对应一种PD
// 2) 每个值的每个元素对应配置文件的路径
type ProblemDaemonConfigPathMap map[ProblemDaemonType]*[]string

// 每种PD负责提供一个下面的结构的实例，其函数指针作为实例化PD的工厂
type ProblemDaemonHandler struct {
	CreateProblemDaemonOrDie func(string) Monitor
	// 描述如何从命令行实例化PD
	CmdOptionDescription string
}

// Exporter的类型
type ExporterType string

// 每种Exporter负责提供一个下面的结构的实例，其函数指针作为实例化Exporter的工厂
type ExporterHandler struct {
	CreateExporterOrDie func(CommandLineOptions) Exporter
	// 描述如何从命令行实例化PD
	Options CommandLineOptions
}

// 用于注入命令行选项
type CommandLineOptions interface {
	SetFlags(*pflag.FlagSet)
} </pre>
<div class="blog_h3"><span class="graybg">入口点</span></div>
<pre class="crayon-plain-tag">func main() {
	// ...
	// 读取并验证命令行选项

	// 初始化所有配置的PD，参数类型为map[ProblemDaemonType]*[]string，每种PD可提供多个配置文件
    // 每种PD都有对应的ProblemDaemonHandler，调用其CreateProblemDaemonOrDie方法、传入配置文件
    // 并创建Goroutine
	problemDaemons := problemdaemon.NewProblemDaemons(npdo.MonitorConfigPaths)
	// ...
	// 初始化所有Exporters
	defaultExporters := []types.Exporter{}
	if ke := k8sexporter.NewExporterOrDie(npdo); ke != nil {
		defaultExporters = append(defaultExporters, ke)
	}
	if pe := prometheusexporter.NewExporterOrDie(npdo); pe != nil {
		defaultExporters = append(defaultExporters, pe)
	}
	// K8S、Prometheus是内置Exporter，还可以支持可拔插的Experters。
	plugableExporters := exporters.NewExporters()
	npdExporters := []types.Exporter{}
	npdExporters = append(npdExporters, defaultExporters...)
	npdExporters = append(npdExporters, plugableExporters...)

	// 初始化NPD核心并启动
	p := problemdetector.NewProblemDetector(problemDaemons, npdExporters)
	if err := p.Run(); err != nil {
		glog.Fatalf("Problem detector failed with error: %v", err)
	}
}</pre>
<div class="blog_h3"><span class="graybg">初始化Monitors</span></div>
<p>NPD要求至少启用一个PD，否则NPD就没有输入，没有实际意义。具体需要初始化哪些PD，取决于你提供的命令行参数。</p>
<pre class="crayon-plain-tag">func NewProblemDaemons(monitorConfigPaths types.ProblemDaemonConfigPathMap) []types.Monitor {
	problemDaemonMap := make(map[string]types.Monitor)
	// 遍历配置
	for problemDaemonType, configs := range monitorConfigPaths {
		// 处理每个PD类型
		for _, config := range *configs {
			if _, ok := problemDaemonMap[config]; ok {
				// 跳过重复配置
				continue
			}
			// 为每个PD的每个配置文件创建PD实例，                       调用工厂函数
			problemDaemonMap[config] = handlers[problemDaemonType].CreateProblemDaemonOrDie(config)
		}
	}

	problemDaemons := []types.Monitor{}
	for _, problemDaemon := range problemDaemonMap {
		problemDaemons = append(problemDaemons, problemDaemon)
	}
	// 返回PD列表
	return problemDaemons
}</pre>
<div class="blog_h3"><span class="graybg">初始化Exporters</span></div>
<p>如果启用了Kubernertes Exporter，检测到的节点问题将报告为K8S的NodeCondition和Event。启动此Exporter的代码如下：</p>
<pre class="crayon-plain-tag">func NewExporterOrDie(npdo *options.NodeProblemDetectorOptions) types.Exporter {
	// ...
	// 创建一个问题客户端，此客户端能够读写当前节点的问题、事件
	c := problemclient.NewClientOrDie(npdo)

	// 连接到K8S API Server
	waitForAPIServerReadyWithTimeout(c, npdo)

	ke := k8sExporter{
		client:           c,
		// ConditionManager利用ProblemClient，将节点状态同步给API Server
		conditionManager: condition.NewConditionManager(c, clock.RealClock{}),
	}

	// 启动一个HTTP服务，在端点/conditions提供NodeCondition查询功能
	ke.startHTTPReporting(npdo)
	// 启动一个异步线程，定期同步到API Server
	ke.conditionManager.Start()

	return &amp;ke
}</pre>
<p>如果启用了Prometheus Exporter，则会启动一个HTTP服务器，供Prometheus Server来抓取指标：</p>
<pre class="crayon-plain-tag">func NewExporterOrDie(npdo *options.NodeProblemDetectorOptions) types.Exporter {
	// ...
	addr := net.JoinHostPort(npdo.PrometheusServerAddress, strconv.Itoa(npdo.PrometheusServerPort))
	// 创建Prometheus的Exporter对象，它实现server.Handler
	pe, err := prometheus.NewExporter(prometheus.Options{})
	go func() {
		mux := http.NewServeMux()
		// 处理Exporter请求
		mux.Handle("/metrics", pe)
		if err := http.ListenAndServe(addr, mux); err != nil {
		}
	}()
	// 集成OpenCensus
	view.RegisterExporter(pe)
	return &amp;prometheusExporter{}
}</pre>
<div class="blog_h3"><span class="graybg">启动NPD </span></div>
<p>入口点的最后一步是初始化NPD核心。NPD会持有所有Monitors、Exporters：</p>
<pre class="crayon-plain-tag">type problemDetector struct {
	monitors  []types.Monitor
	exporters []types.Exporter
}</pre>
<p>NPD的外部接口很简单：</p>
<pre class="crayon-plain-tag">type ProblemDetector interface {
	// 运行NPD
	Run() error
}</pre>
<p>我们看一下Run方法的实现：</p>
<pre class="crayon-plain-tag">func (p *problemDetector) Run() error {
	// 逐个启动Monitors
	// 所有Monitor的输出通道
	var chans []&lt;-chan *types.Status
	failureCount := 0
	for _, m := range p.monitors {
		// 启动Monitor
		ch, err := m.Start()
		if err != nil {
			// 失败，尝试下一个
			failureCount += 1
			continue
		}
		if ch != nil {
			// 保存输出通道
			chans = append(chans, ch)
		}
	}
	if len(p.monitors) == failureCount {
		// 所有PD都启动失败，失败
		return fmt.Errorf("no problem daemon is successfully setup")
	}
	// 监听所有PD的输出通道，并将其中的Status归集到单个通道ch中
	ch := groupChannel(chans)
	glog.Info("Problem detector started")

	// 收集到的PD输出，必须交给Exporter进行处理，才有价值
	for {
		select {
		case status := &lt;-ch:
			for _, exporter := range p.exporters {
				exporter.ExportProblems(status)
			}
		}
	}
}

// 为每个PD的输出通道创建Goroutine
// 这些Goroutine接收到PD的状态报告后，将其合并到单个通道
func groupChannel(chans []&lt;-chan *types.Status) &lt;-chan *types.Status {
	statuses := make(chan *types.Status)
	for _, ch := range chans {
		go func(c &lt;-chan *types.Status) {
			for status := range c {
				statuses &lt;- status
			}
		}(ch)
	}
	return statuses
}</pre>
<div class="blog_h3"><span class="graybg">扩展Monitors</span></div>
<p>入口点中的语句：<pre class="crayon-plain-tag">problemdaemon.NewProblemDaemons(npdo.MonitorConfigPaths)</pre>负责初始化所有配置的PD。</p>
<p>对于每种PD的每个配置文件，都会调用：ProblemDaemonHandler.CreateProblemDaemonOrDie进行PD实例化：</p>
<pre class="crayon-plain-tag">problemDaemonMap[config] = handlers[problemDaemonType].CreateProblemDaemonOrDie(config)

type ProblemDaemonHandler struct {
	// 创建PD
	CreateProblemDaemonOrDie func(string) Monitor
	// 命令行选项
	CmdOptionDescription string
}</pre>
<p>开发自己的PD时，你需要提供ProblemDaemonHandler的实例并调用problemdaemon.Register进行注册：</p>
<pre class="crayon-plain-tag">var (
	handlers = make(map[types.ProblemDaemonType]types.ProblemDaemonHandler)
)

func Register(problemDaemonType types.ProblemDaemonType, handler types.ProblemDaemonHandler) {
	handlers[problemDaemonType] = handler
}</pre>
<p>同时，提供Monitor接口的实现。 </p>
<div class="blog_h3"><span class="graybg">扩展Exporters</span></div>
<p>入口点中的语句：<pre class="crayon-plain-tag">plugableExporters := exporters.NewExporters()</pre>负责初始化扩展的Exporters，Stackdriver Exporter就是这样的一种扩展。</p>
<p>NewExporters的逻辑很简单，遍历一个集合，取出其中的ExporterHandler并实例化Exporter：</p>
<pre class="crayon-plain-tag">func NewExporters() []types.Exporter {
	exporters := []types.Exporter{}
	for _, handler := range handlers {
		exporter := handler.CreateExporterOrDie(handler.Options)
		if exporter == nil {
			continue
		}
		exporters = append(exporters, exporter)
	}
	return exporters
}


// ExporterHandler是结构，不是接口。每个Exporter插件需要指定
// 一个函数指针，作为创建Exporter的工厂函数
type ExporterHandler struct {
	CreateExporterOrDie func(CommandLineOptions) Exporter
	Options CommandLineOptions
}</pre>
<p>exporters包对外暴露了注册扩展Exporter的接口：</p>
<pre class="crayon-plain-tag">var (
	handlers = make(map[types.ExporterType]types.ExporterHandler)
)

func Register(exporterType types.ExporterType, handler types.ExporterHandler) {
	handlers[exporterType] = handler
}</pre>
<p>开发自己的Exporter时，你需要调用上面的Register注册Exporter的工厂函数，同时实现Exporter接口。</p>
<div class="blog_h3"><span class="graybg">Metric</span></div>
<p>NPD对指标这一概念也进行了封装，它依赖OpenCensus而不是Prometheus这样具体的实现的API。</p>
<p>OpenCensus是一个开源项目，对比OpenTracing，在Tracing的基础上加了Metrics功能。现在两个项目已经合并为OpenTelemetry并进入CNCF沙箱。OpenTelemetry统一了数据格式规范、SDK，推荐用Prometheus作为Metrics后端，Jaeger做Tracing后端。</p>
<p>所有指标如下：</p>
<pre class="crayon-plain-tag">const (
	ProblemCounterID  MetricID = "problem_counter"
	ProblemGaugeID    MetricID = "problem_gauge"
	DiskIOTimeID      MetricID = "disk/io_time"
	DiskWeightedIOID  MetricID = "disk/weighted_io"
	DiskAvgQueueLenID MetricID = "disk/avg_queue_len"
	HostUptimeID      MetricID = "host/uptime"
)</pre>
<p>前两个是针对所有Problem的Counter/Gauge，后面几个都是SystemStatsMonitor暴露的指标。</p>
<p>NPD定义了两种数据类型的指标Int64Metric、Float64Metric，前者代码如下：</p>
<pre class="crayon-plain-tag">package metrics

import (
	"context"
	"fmt"

	"go.opencensus.io/stats"
	"go.opencensus.io/stats/view"
	"go.opencensus.io/tag"
)

// Int64MetricRepresentation 表示一个int64类型的指标的快照值
type Int64MetricRepresentation struct {
	// 指标名
	Name string
	// 指标标签集
	Labels map[string]string
	// 指标的顺时值
	Value int64
}

// Int64Metric 表示一个int64类型的指标
type Int64Metric struct {
	// 指标名
	name    string
	// OpenCensus中的概念，本质上就是一个描述符
	measure *stats.Int64Measure
}

// 工厂函数，创建指标
func NewInt64Metric(metricID MetricID, viewName string, description string, unit string, aggregation Aggregation, tagNames []string) (*Int64Metric, error) {
	// OpenCensus中的View
	if viewName == "" {
		return nil, nil
	}
	// 建立指标ID和视图名的对应关系
	MetricMap.AddMapping(metricID, viewName)
	// 将标签名转换为OpenCensus的Tag键
	tagKeys, err := getTagKeysFromNames(tagNames)

	// 将NPD的Aggregation转换为OpenCensus的Aggregation
	// OpenCensus中的Aggregation表示聚合值的方法
	var aggregationMethod *view.Aggregation
	switch aggregation {
	case LastValue:
		aggregationMethod = view.LastValue() // 仅仅报告最后记录的值
	case Sum:
		aggregationMethod = view.Sum() // 对所有收集的值进行求和
	default:
		return nil, fmt.Errorf("unknown aggregation option %q", aggregation)
	}
	// 创建Int64Measure度量
	measure := stats.Int64(viewName, description, unit)
	// 创建上述度量的视图
	newView := &amp;view.View{
		Name:        viewName,
		Measure:     measure,
		Description: description,
		Aggregation: aggregationMethod,
		TagKeys:     tagKeys,
	}
	// 注册此度量的描述符measureDescriptor
	view.Register(newView)
	// 返回NPD的封装
	metric := Int64Metric{viewName, measure}
	return &amp;metric, nil
}

// 为指标记录一个度量，并使用提供的Tag作为指标标签
func (metric *Int64Metric) Record(tags map[string]string, measurement int64) error {
	// Mutator能够对tag map 进行变换
	var mutators []tag.Mutator

	tagMapMutex.RLock()
	defer tagMapMutex.RUnlock()

	for tagName, tagValue := range tags {
		tagKey, ok := tagMap[tagName]
		if !ok {
			return fmt.Errorf("referencing none existing tag %q in metric %q", tagName, metric.name)
		}
		// 添加这样的Mutator，如果tagKey存在则更新，否则插入
		mutators = append(mutators, tag.Upsert(tagKey, tagValue))
	}

	// RecordWithTags能够一次性记录一个或多个度量值
	return stats.RecordWithTags(
		context.Background(),
		// 提供Tag
		mutators,
		// 调用*stats.Int64Measure的M方法，可以创建一个Measurement
		// Measurement的本质是一个值，同时包含Int64Measure及其measureDescriptor的引用
		metric.measure.M(measurement))
}</pre>
<p>OpenCensus中各种概念比较繁琐，讲清楚需要独立开一篇文章。这里牵涉到的有：</p>
<ol>
<li>tag.Mutator：这个接口负责为度量值生成标签（名、值对）</li>
<li>stats.Measure：度量，此接口表示一个指标，和Prometheus中的Metric对应。度量只具有名称、描述、单位三个属性，不包含标签，或者值。每种度量都提供了方法来创建度量值，例如Int64Measure.M方法将int64转换为Measurement。度量对外不可见，要将度量值导出，必须使用视图。如果没有为Measure定义视图，则记录Measure的成本非常低</li>
<li>stats.Measurement：度量值，此接口表示Measure的一个具体的采集值。</li>
<li>view.View：视图，用于聚合、对外展示已经记录的度量值。视图具有唯一性的名称、关联唯一的度量、具有确定的标签名集合，以及一个确定的聚合函数</li>
</ol>
<p>如果从上面的stats.RecordWithTags调用跟踪下去，可以看到OpenCensus最终仅仅会调用一个<pre class="crayon-plain-tag">internal.DefaultRecorder</pre>这个函数：</p>
<pre class="crayon-plain-tag">func record(tags *tag.Map, ms interface{}, attachments map[string]interface{}) {
	req := &amp;recordReq{
		tm:          tags,
		ms:          ms.([]stats.Measurement),
		attachments: attachments,
		t:           time.Now(),
	}
	defaultWorker.c &lt;- req
}</pre>
<p>可以看到此函数构建一个记录请求，并从通道发出。接收此请求并处理的Goroutine如下：</p>
<pre class="crayon-plain-tag">func (w *worker) start() {
	// 全局的Metrics生产者管理器
	prodMgr := metricproducer.GlobalManager()
	// 注册自己
	prodMgr.AddProducer(w)

	for {
		select {
		case cmd := &lt;-w.c:
			// 处理记录请求
			cmd.handleCommand(w)
		case &lt;-w.timer.C:
			w.reportUsage(time.Now())
		case &lt;-w.quit:
			w.timer.Stop()
			close(w.c)
			w.done &lt;- true
			return
		}
	}
}

func (cmd *recordReq) handleCommand(w *worker) {
	// Worker锁
	w.mu.Lock()
	defer w.mu.Unlock()
	// 一个请求中可以具有多个stats.Measurement
	for _, m := range cmd.ms {
		if (m == stats.Measurement{}) {
			continue
		}
		// 获取度量值的度量的所有视图
		ref := w.getMeasureRef(m.Measure().Name())
		for v := range ref.views {
			// 向所有视图添加此度量，内部会调用aggregator.addSample
			v.addSample(cmd.tm, m.Value(), cmd.attachments, time.Now())
		}
	}
}</pre>
<p>最终，记录的、聚合后的指标值，就是放在View中的。</p>
<p>那么，外部怎么访问这些值？其实，你打开<a href="http://127.0.0.1:20257/metrics">http://127.0.0.1:20257/metrics</a>可以看到，Prometheus的Exporter已经获取到OpenCensus记录的数据了。那么Prometheus和OpenCensus是如何配合的呢？</p>
<p>Prometheus在处理/metrics请求时，会调用prometheus.Gatherer，此接口的实现是prometheus.Registry。在NPD启动期间，它会创建一个Registry：</p>
<pre class="crayon-plain-tag">func NewExporter(o Options) (*Exporter, error) {
	if o.Registry == nil {
		// 创建Prometheus注册表
		o.Registry = prometheus.NewRegistry()
	}
	// 创建指标收集器
	collector := newCollector(o, o.Registry)
	// ...
}

func newCollector(opts Options, registrar *prometheus.Registry) *collector {
	return &amp;collector{
		reg:    registrar,
		opts:   opts,
		// 通过此Reader读取OpenCensus采集的数据
		reader: metricexport.NewReader()}
}</pre>
<p>通过上述代码可以看到Prometheus如何和OpenCensus集成的，它们之间的接口是metricexport.Reader，此接口是OpenCensus提供的，Prometheus依赖于此接口。</p>
<p>metricexport.NewReader()调用创建的Reader具有读取OpenCensus采集的数据的能力。 </p>
<div class="blog_h3"><span class="graybg">Tomb</span></div>
<p>用于从外部控制协程的生命周期， 它的逻辑很简单，准备结束生命周期时：</p>
<ol>
<li>外部协作者发起一个通知</li>
<li>协作线程接收到通知，进行清理</li>
<li>清理完成后，协程反向通知外部协作者</li>
<li>外部协作者退出阻塞</li>
</ol>
<pre class="crayon-plain-tag">package tomb

type Tomb struct {
	stop chan struct{}  // 当生命周期结束时，外部关闭此通道，以通知协程
	done chan struct{}  // 当协程完成清理后，关闭此通道，以通知Stop的调用者
}

func NewTomb() *Tomb {
	return &amp;Tomb{
		stop: make(chan struct{}),
		done: make(chan struct{}),
	}
}

// 从外部（另外一个Goroutine）进行阻塞性的关闭操作
func (t *Tomb) Stop() {
	close(t.stop)
	&lt;-t.done
}

// 简单的返回Stop通道，如果已经通知关闭，则此读取此通道不会阻塞
func (t *Tomb) Stopping() &lt;-chan struct{} {
	return t.stop
}

// 协程内部负责调用此函数，反向通知协作者，告诉它清理工作已经完成
func (t *Tomb) Done() {
	close(t.done)
} </pre>
<div class="blog_h2"><span class="graybg">NPD源码分析</span></div>
<div class="blog_h3"><span class="graybg">SystemLogMonitor</span></div>
<p>此PD能够分析各种形式的日志，读取其内容，使用正则式匹配来发现节点故障。主要代码如下：</p>
<pre class="crayon-plain-tag">package systemlogmonitor

import (
	"encoding/json"
	"io/ioutil"
	"time"

	"github.com/golang/glog"

	"k8s.io/node-problem-detector/pkg/problemdaemon"
	"k8s.io/node-problem-detector/pkg/problemmetrics"
	"k8s.io/node-problem-detector/pkg/systemlogmonitor/logwatchers"
	watchertypes "k8s.io/node-problem-detector/pkg/systemlogmonitor/logwatchers/types"
	logtypes "k8s.io/node-problem-detector/pkg/systemlogmonitor/types"
	systemlogtypes "k8s.io/node-problem-detector/pkg/systemlogmonitor/types"
	"k8s.io/node-problem-detector/pkg/types"
	"k8s.io/node-problem-detector/pkg/util"
	"k8s.io/node-problem-detector/pkg/util/tomb"
)

const SystemLogMonitorName = "system-log-monitor"

// 初始化函数用于注册此PD的工厂函数
func init() {
	problemdaemon.Register(
		SystemLogMonitorName,
		types.ProblemDaemonHandler{
			CreateProblemDaemonOrDie: NewLogMonitorOrDie,
			CmdOptionDescription:     "Set to config file paths."})
}

type logMonitor struct {
	// 配置文件路径
	configPath string
	// 读取日志的逻辑委托给LogWatcher，这里解耦的目的是支持多种类型的日志
	watcher    watchertypes.LogWatcher
	// 日志缓冲，读取的日志在此等待处理
	buffer     LogBuffer
	// 对应配置文件中的字段
	config     MonitorConfig
	// 对应配置文件中的conditions字段
	conditions []types.Condition
	// 输入日志条目的通道
	logCh      &lt;-chan *logtypes.Log
	// 输出状态的通道
	output     chan *types.Status
	// 墓碑，用于控制此Monitor的生命周期
	tomb       *tomb.Tomb
}

// 创建实例，如果失败则panic
func NewLogMonitorOrDie(configPath string) types.Monitor {
	// 创建实例
	l := &amp;logMonitor{
		configPath: configPath,
		tomb:       tomb.NewTomb(),
	}
	// 读取配置文件
	f, err := ioutil.ReadFile(configPath)
	// 作为JSON解析为MonitorConfig
	err = json.Unmarshal(f, &amp;l.config)
	// 设置MonitorConfig的默认值
	(&amp;l.config).ApplyDefaultConfiguration()
	err = l.config.ValidateRules()
	
	// 创建LogWatcher
	l.watcher = logwatchers.GetLogWatcherOrDie(l.config.WatcherConfig)
	// 设置缓冲区
	l.buffer = NewLogBuffer(l.config.BufferSize)
	// 写死的最大通道容量
	l.output = make(chan *types.Status, 1000)
	// 如果启用指标报告
	if *l.config.EnableMetricsReporting {
		// 则为所有类型的Problem（Rule.Reason，比NodeCondition更细粒度）初始化指标
		// Perm类型的初始化一个Gauge指标，一个Counter指标
		// Temp类型的仅仅初始化一个Counter指标
		initializeProblemMetricsOrDie(l.config.Rules)
	}
	return l
}

// 初始化指标
func initializeProblemMetricsOrDie(rules []systemlogtypes.Rule) {
	for _, rule := range rules {
		if rule.Type == types.Perm {
			err := problemmetrics.GlobalProblemMetricsManager.SetProblemGauge(rule.Condition, rule.Reason, false)
		}
		err := problemmetrics.GlobalProblemMetricsManager.IncrementProblemCounter(rule.Reason, 0)
	}
}

// 启动
func (l *logMonitor) Start() (&lt;-chan *types.Status, error) {
	var err error
	// 启动LogWatcher，监控日志的变化
	l.logCh, err = l.watcher.Watch()
	if err != nil {
		return nil, err
	}
	// 启动主循环
	go l.monitorLoop()
	return l.output, nil
}

// 停止
func (l *logMonitor) Stop() {
	// 关闭Stop通道，然后等待Done通道完成
	l.tomb.Stop()
}

// 主循环
func (l *logMonitor) monitorLoop() {
	// 主循环退出后，接触Stop()调用者的阻塞
	defer l.tomb.Done()
	// 初始化状态（Event和Condition）
	l.initializeStatus()
	// 循环
	for {
		select {
		// 日志可用，解析日志
		case log := &lt;-l.logCh:
			l.parseLog(log)
		// Stop通道可读，意味着通知关闭了
		case &lt;-l.tomb.Stopping():
			// 关闭LogWatcher
			l.watcher.Stop()
			glog.Infof("Log monitor stopped: %s", l.configPath)
			return
		}
	}
}

// 解析日志行
func (l *logMonitor) parseLog(log *logtypes.Log) {
	// 一旦新日志行可用，就将其推送到日志缓冲
	l.buffer.Push(log)
	for _, rule := range l.config.Rules {
		// 然后逐个规则去匹配
		matched := l.buffer.Match(rule.Pattern)
		if len(matched) == 0 {
			continue
		}
		// 如果匹配规则，则报告规则中声明的状态
		status := l.generateStatus(matched, rule)
		glog.Infof("New status generated: %+v", status)
		// 输出状态
		l.output &lt;- status
	}
}

// 从日志生成Status
func (l *logMonitor) generateStatus(logs []*logtypes.Log, rule systemlogtypes.Rule) *types.Status {
	// 第一行日志的时间戳作为状态的时间戳
	timestamp := logs[0].Timestamp
	// 读取日志内容
	message := generateMessage(logs)
	var events []types.Event
	var changedConditions []*types.Condition
	if rule.Type == types.Temp {
		// 对于临时问题，仅仅生成事件
		events = append(events, types.Event{
			Severity:  types.Warn,
			Timestamp: timestamp,
			Reason:    rule.Reason,
			Message:   message,
		})
	} else {
		// 对于永久问题，改变Condition
		for i := range l.conditions {
			condition := &amp;l.conditions[i]
			// 找到匹配的、此Monitor定义的Condition
			if condition.Type == rule.Condition {
				// 如果Condition改变（Status或Reson字段变了）
				if condition.Status == types.False || condition.Reason != rule.Reason {
					// 则更新时间戳和消息
					condition.Transition = timestamp
					condition.Message = message
					// 并发布事件
					events = append(events, util.GenerateConditionChangeEvent(
						condition.Type,
						types.True,
						rule.Reason,
						timestamp,
					))
				}
				condition.Status = types.True
				condition.Reason = rule.Reason
				changedConditions = append(changedConditions, condition)
				break
			}
		}
	}
	// 报告Problem数量指标
	if *l.config.EnableMetricsReporting {
		for _, event := range events {
			err := problemmetrics.GlobalProblemMetricsManager.IncrementProblemCounter(event.Reason, 1)
		}
		for _, condition := range changedConditions {
			err := problemmetrics.GlobalProblemMetricsManager.SetProblemGauge(
				condition.Type, condition.Reason, condition.Status == types.True)
			}
		}
	}
	// 处于性能考虑，应该聚合Event、Condition，周期性的报告，而非这样立即报告
	return &amp;types.Status{
		Source: l.config.Source,
		Events:     events,
		Conditions: l.conditions,
	}
}

// 初始化状态并报告一次
func (l *logMonitor) initializeStatus() {
	// 初始化默认Condition，来自配置的condition字段
	l.conditions = initialConditions(l.config.DefaultConditions)
	l.output &lt;- &amp;types.Status{
		Source:     l.config.Source,
		Conditions: l.conditions,
	}
}

func initialConditions(defaults []types.Condition) []types.Condition {
	conditions := make([]types.Condition, len(defaults))
	copy(conditions, defaults)
	for i := range conditions {
		conditions[i].Status = types.False
		conditions[i].Transition = time.Now()
	}
	return conditions
}

func generateMessage(logs []*logtypes.Log) string {
	messages := []string{}
	for _, log := range logs {
		messages = append(messages, log.Message)
	}
	return concatLogs(messages)
}</pre>
<p>和配置相关的代码如下，MonitorConfig嵌入WatcherConfig，正好和配置文件结构对应：</p>
<pre class="crayon-plain-tag">// node-problem-detector/pkg/systemlogmonitor/config.go
type MonitorConfig struct {
	// LogWatcher（LogMonitor插件）的配置
	watchertypes.WatcherConfig
	// 缓冲（行数）大小
	BufferSize int `json:"bufferSize"`
	// 此PD的名称
	Source string `json:"source"`
	// 此PD处理的所有Condition的默认状态
	DefaultConditions []types.Condition `json:"conditions"`
	// 日志匹配规则列表
	Rules []systemlogtypes.Rule `json:"rules"`
	// 是否将Problem报告为指标
	EnableMetricsReporting *bool `json:"metricsReporting,omitempty"`
}

// node-problem-detector/pkg/systemlogmonitor/logwatchers/types/log_watcher.go
type WatcherConfig struct {
	// 插件类型，可选 filelog, journald, kmsg
	Plugin string `json:"plugin,omitempty"`
	// 键值对形式的插件配置，具体可以包含哪些配置项，取决于插件
	PluginConfig map[string]string `json:"pluginConfig,omitempty"`
	// 日志的路径
	LogPath string `json:"logPath,omitempty"`
	// 向当前时间点往前看多久日志
	Lookback string `json:"lookback,omitempty"`
	// 仅仅查看节点启动之后多久的日志，可以避免启动期间的不稳定状态触发不必要的问题报告
	Delay string `json:"delay,omitempty"`
}</pre>
<p>LogWatcher的实现有多种，它们具有统一的接口：</p>
<pre class="crayon-plain-tag">type LogWatcher interface {
	// 开始监控日志，并通过通道输出日志
	Watch() (&lt;-chan *types.Log, error)
	// 停止，注意释放打开的资源
	Stop()
}

// LogWatcher工厂函数
type WatcherCreateFunc func(WatcherConfig) LogWatcher</pre>
<p>类似于Monitor、Exporter，LogWatcher也需要注册：</p>
<pre class="crayon-plain-tag">// 注册表
var createFuncs = map[string]types.WatcherCreateFunc{}

// 注册函数，比较奇葩的是名称没有导出，因此各种LogWatcher的注册均是在logwatchers包中进行的（而非各LogWatcher自己的包）
func registerLogWatcher(name string, create types.WatcherCreateFunc) {
	createFuncs[name] = create
}

// 根据config.Plugin字段来查找注册表，获取LogWatcher的工厂函数
func GetLogWatcherOrDie(config types.WatcherConfig) types.LogWatcher {
	create, ok := createFuncs[config.Plugin]
	return create(config)
}</pre>
<p>我们不去逐个分析LogWatcher的实现，仅以kernelLogWatcher（ksmg）为例：  </p>
<pre class="crayon-plain-tag">package kmsg

import (
	"fmt"
	"strings"
	"time"

	utilclock "code.cloudfoundry.org/clock"
	"github.com/euank/go-kmsg-parser/kmsgparser"
	"github.com/golang/glog"

	"k8s.io/node-problem-detector/pkg/systemlogmonitor/logwatchers/types"
	logtypes "k8s.io/node-problem-detector/pkg/systemlogmonitor/types"
	"k8s.io/node-problem-detector/pkg/util"
	"k8s.io/node-problem-detector/pkg/util/tomb"
)

type kernelLogWatcher struct {
	cfg       types.WatcherConfig
	startTime time.Time
	logCh     chan *logtypes.Log
	tomb      *tomb.Tomb

	kmsgParser kmsgparser.Parser
	clock      utilclock.Clock
}

// 工厂函数
func NewKmsgWatcher(cfg types.WatcherConfig) types.LogWatcher {
	// 获取系统启动到现在过了多久
	uptime, err := util.GetUptimeDuration()
	// 判断何时才应该开始监控日志	
	startTime, err := util.GetStartTime(time.Now(), uptime, cfg.Lookback, cfg.Delay)
	return &amp;kernelLogWatcher{
		cfg:       cfg,
		startTime: startTime,
		tomb:      tomb.NewTomb(),
		logCh: make(chan *logtypes.Log, 100),
		clock: utilclock.NewClock(),
	}
}

// 确保签名匹配
var _ types.WatcherCreateFunc = NewKmsgWatcher

// 开始监控
func (k *kernelLogWatcher) Watch() (&lt;-chan *logtypes.Log, error) {
	if k.kmsgParser == nil {
		// 初始化内核日志解析器
		parser, err := kmsgparser.NewParser()
		k.kmsgParser = parser
	}
	// 异步启动主监控循环
	go k.watchLoop()
	return k.logCh, nil
}

// 停止监控
func (k *kernelLogWatcher) Stop() {
	// 停止解析器
	k.kmsgParser.Close()
	// 发起停止信号，并等待主监控循环的通知
	k.tomb.Stop()
}

// 主监控循环
func (k *kernelLogWatcher) watchLoop() {
	// 退出时关闭输出通道，并且通过Tomb告知清理结束，Stop方法可以返回了
	defer func() {
		close(k.logCh)
		k.tomb.Done()
	}()
	// go-kmsg-parser项目提供的功能，获得一个可读通道，从中可以读取到内核消息
	kmsgs := k.kmsgParser.Parse()

	for {
		select {
		// 停止信号，清理
		case &lt;-k.tomb.Stopping():
			// 关闭内核消息解析器
			if err := k.kmsgParser.Close(); err != nil {
			}
			return
		// 获取内核消息
		case msg := &lt;-kmsgs:
			// 跳过空消息
			if msg.Message == "" {
				continue
			}

			// 对于过早的消息，丢弃
			if msg.Timestamp.Before(k.startTime) {
				continue
			}
			// 输出消息
			k.logCh &lt;- &amp;logtypes.Log{
				Message:   strings.TrimSpace(msg.Message),
				Timestamp: msg.Timestamp,
			}
		}
	}
}</pre>
<div class="blog_h3"><span class="graybg">SystemStatsMonitor</span></div>
<p>此PD仅仅产生Metrics，而不报告Problem，因此其Start方法返回nil。它报告指标时调用的是OpenCensus的API。</p>
<p>需要注意的是，<span style="background-color: #c0c0c0;">NPD的Exporter是针对Problem的</span>，Monitor可以产生，也可以不产生Problem（Status对象）。不产生Problem的Monitor和Exporter直接关系。</p>
<pre class="crayon-plain-tag">const SystemStatsMonitorName = "system-stats-monitor"

// 注册
func init() {
	problemdaemon.Register(SystemStatsMonitorName, types.ProblemDaemonHandler{
		CreateProblemDaemonOrDie: NewSystemStatsMonitorOrDie,
		CmdOptionDescription:     "Set to config file paths."})
}

type systemStatsMonitor struct {
	// 配置文件路径
	configPath    string
	// 从文件中读取到的配置
	config        ssmtypes.SystemStatsConfig
	// 统计信息收集器，目前仅仅支持磁盘、主机信息
	diskCollector *diskCollector
	hostCollector *hostCollector
	// 生命周期控制
	tomb          *tomb.Tomb
}

// 工厂
func NewSystemStatsMonitorOrDie(configPath string) types.Monitor {
	ssm := systemStatsMonitor{
		configPath: configPath,
		tomb:       tomb.NewTomb(),
	}

	// 读取、应用、验证配置
	f, err := ioutil.ReadFile(configPath)
	err = json.Unmarshal(f, &amp;ssm.config)
	err = ssm.config.ApplyConfiguration()
	err = ssm.config.Validate()

	// 按需创建收集器
	if len(ssm.config.DiskConfig.MetricsConfigs) &gt; 0 {
		ssm.diskCollector = NewDiskCollectorOrDie(&amp;ssm.config.DiskConfig)
	}
	if len(ssm.config.HostConfig.MetricsConfigs) &gt; 0 {
		ssm.hostCollector = NewHostCollectorOrDie(&amp;ssm.config.HostConfig)
	}
	return &amp;ssm
}

// 异步启动主循环
func (ssm *systemStatsMonitor) Start() (&lt;-chan *types.Status, error) {
	go ssm.monitorLoop()
	return nil, nil
}

func (ssm *systemStatsMonitor) monitorLoop() {
	// 通知Stop()调用者
	defer ssm.tomb.Done()
	
	// 定时器
	runTicker := time.NewTicker(ssm.config.InvokeInterval)
	defer runTicker.Stop()

	// 立即进行一次采集
	select {
	case &lt;-ssm.tomb.Stopping():
		return
	default:
		ssm.diskCollector.collect()
		ssm.hostCollector.collect()
	}

	// 定时采集
	for {
		select {
		case &lt;-runTicker.C:
			ssm.diskCollector.collect()
			ssm.hostCollector.collect()
		case &lt;-ssm.tomb.Stopping():
			return
		}
	}
}

func (ssm *systemStatsMonitor) Stop() {
	ssm.tomb.Stop()
}</pre>
<p>可以看到，此PD只是定期调用收集器的collect方法，并且不从此方法获取任何信息。</p>
<p>目前Collector有两个，以DisCollector为例，我们看一下它将收集的指标输出到何处了：</p>
<pre class="crayon-plain-tag">package systemstatsmonitor

import (
	"context"
	"os/exec"
	"strings"
	"time"

	"github.com/golang/glog"
	"github.com/shirou/gopsutil/disk"

	ssmtypes "k8s.io/node-problem-detector/pkg/systemstatsmonitor/types"
	"k8s.io/node-problem-detector/pkg/util/metrics"
)

const deviceNameLabel = "device_name"

type diskCollector struct {
	// IO时间
	mIOTime      *metrics.Int64Metric
	// 加权IO时间，此字段上次刷新以来，消耗在IO上的毫秒数 * 进行中的IO操作数量
	mWeightedIO  *metrics.Int64Metric
	// 平均队列长度
	mAvgQueueLen *metrics.Float64Metric
	// 配置信息
	config *ssmtypes.DiskStatsConfig

	// IO时间历时记录
	historyIOTime     map[string]uint64
	historyWeightedIO map[string]uint64
}


// 工厂函数
func NewDiskCollectorOrDie(diskConfig *ssmtypes.DiskStatsConfig) *diskCollector {
	dc := diskCollector{config: diskConfig}

	var err error

	// 创建NPD封装的Metrics对象
	dc.mIOTime, err = metrics.NewInt64Metric(
		metrics.DiskIOTimeID,
		// displayName作为OpenCensus View名
		diskConfig.MetricsConfigs[string(metrics.DiskIOTimeID)].DisplayName,
		"The IO time spent on the disk",
		"second",
		// 求和聚合
		metrics.Sum,
		[]string{deviceNameLabel})

	dc.mWeightedIO, err = metrics.NewInt64Metric(
		metrics.DiskWeightedIOID,
		diskConfig.MetricsConfigs[string(metrics.DiskWeightedIOID)].DisplayName,
		"The weighted IO on the disk",
		"second",
		metrics.Sum,
		[]string{deviceNameLabel})

	dc.mAvgQueueLen, err = metrics.NewFloat64Metric(
		metrics.DiskAvgQueueLenID,
		diskConfig.MetricsConfigs[string(metrics.DiskAvgQueueLenID)].DisplayName,
		"The average queue length on the disk",
		"second",
		metrics.LastValue,
		[]string{deviceNameLabel})

	dc.historyIOTime = make(map[string]uint64)
	dc.historyWeightedIO = make(map[string]uint64)

	return &amp;dc
}

func (dc *diskCollector) collect() {
	if dc == nil {
		return
	}

	blks := []string{}
	// 列出所有磁盘
	// 列出所有非Slave非Holder磁盘
	if dc.config.IncludeRootBlk {
		blks = append(blks, listRootBlockDevices(dc.config.LsblkTimeout)...)
	}
	// 列出所有分区
	if dc.config.IncludeAllAttachedBlk {
		blks = append(blks, listAttachedBlockDevices()...)
	}
	// 调用gopsutil，此项目能够狂平台获取各种操作系统、硬件的指标
	//                           总是递增
	ioCountersStats, err := disk.IOCounters(blks...)
	if err != nil {
		return
	}
	// 迭代所有指标
	for deviceName, ioCountersStat := range ioCountersStats {
		// 根据上一次度量值计算平均队列长度
		lastIOTime := dc.historyIOTime[deviceName]
		lastWeightedIO := dc.historyWeightedIO[deviceName]

		dc.historyIOTime[deviceName] = ioCountersStat.IoTime
		dc.historyWeightedIO[deviceName] = ioCountersStat.WeightedIO
		// 平均队列长度 = (上次加权IO耗时 - 本次加权IO超时) / (上次IO耗时 - 本次IO耗时)
		//              = 平均队列长度 * (上次IO耗时 - 本次IO耗时) / (上次IO耗时 - 本次IO耗时)
		avgQueueLen := float64(0.0)
		if lastIOTime != ioCountersStat.IoTime {
			avgQueueLen = float64(ioCountersStat.WeightedIO-lastWeightedIO) / float64(ioCountersStat.IoTime-lastIOTime)
		}

		// 为指标添加 {"device_name": deviceName} 标签
		tags := map[string]string{deviceNameLabel: deviceName}
		// 这里录制度量时，要使用增量值，因为对应度量已经设置了聚合方法为sum
		if dc.mIOTime != nil {
			dc.mIOTime.Record(tags, int64(ioCountersStat.IoTime-lastIOTime))
		}
		if dc.mWeightedIO != nil {
			dc.mWeightedIO.Record(tags, int64(ioCountersStat.WeightedIO-lastWeightedIO))
		}
		if dc.mAvgQueueLen != nil {
			dc.mAvgQueueLen.Record(tags, avgQueueLen)
		}
	}
}

// 调用lsblk命令列出磁盘
func listRootBlockDevices(timeout time.Duration) []string {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	// 调用命令
	cmd := exec.CommandContext(ctx, "lsblk", "-d", "-n", "-o", "NAME")
	stdout, err := cmd.Output()
	return strings.Split(strings.TrimSpace(string(stdout)), "\n")
}

// 列出所有分区
func listAttachedBlockDevices() []string {
	blks := []string{}

	partitions, err := disk.Partitions(false)
	if err != nil {
		glog.Errorf("Failed to retrieve the list of disk partitions: %v", err)
		return blks
	}

	for _, partition := range partitions {
		blks = append(blks, partition.Device)
	}
	return blks
}</pre>
<div class="blog_h3"><span class="graybg">CustomPluginMonitor</span></div>
<p>此PD为NPD提供了一种插件化机制，允许基于任何语言来编写监控脚本，只需要这些脚本遵循NPD关于退出码和标准输出的规范。</p>
<p>此PD定义了以下类型 / 常量：</p>
<pre class="crayon-plain-tag">package types

import (
	"k8s.io/node-problem-detector/pkg/types"
	"time"
)

type Status int

// 自定义插件的返回码
const (
	OK      Status = 0
	NonOK   Status = 1
	Unknown Status = 2
)

// 插件的检查结果
type Result struct {
	// 提供入参
	Rule       *CustomRule
	// 插件状态
	ExitStatus Status
	// 标准输出
	Message    string
}

// 自定义规则（插件），描述CPM如何调用插件，分析调用结果
type CustomRule struct {
	// 报告永久还是临时问题
	Type types.Type `json:"type"`
	// 此问题触发哪种NodeCondition，仅当永久问题才设置此字段
	Condition string `json:"condition"`
	// 问题的简短原因，对于永久问题，通常描述NodeCondition的一个子类型
	Reason string `json:"reason"`
	// 自定义插件（脚本）的文件路径
	Path string `json:"path"`
	// 传递给自定义插件的参数
	Args []string `json:"args"`
	// 自定义插件执行超时
	TimeoutString *string `json:"timeout"`
	Timeout *time.Duration `json:"-"`
}</pre>
<p>关于如何配置CPM，以及每个插件的代码：</p>
<pre class="crayon-plain-tag">package types

import (
	"fmt"
	"os"
	"time"

	"k8s.io/node-problem-detector/pkg/types"
)

// 配置参数默认值
var (
	// 默认全局超时
	defaultGlobalTimeout                     = 5 * time.Second
	defaultGlobalTimeoutString               = defaultGlobalTimeout.String()
	// 默认调用间隔
	defaultInvokeInterval                    = 30 * time.Second
	defaultInvokeIntervalString              = defaultInvokeInterval.String()
	// 默认最大输出长度
	defaultMaxOutputLength                   = 80
	// 默认并发度
	defaultConcurrency                       = 3
	// 默认是否 状态消息变更导致Condition更新
	defaultMessageChangeBasedConditionUpdate = false
	// 默认是否启用指标报告
	defaultEnableMetricsReporting            = true

	customPluginName = "custom"
)

// 全局配置
type pluginGlobalConfig struct {
	// 所有插件被调用的间隔
	InvokeIntervalString *string `json:"invoke_interval,omitempty"`
	// 全局插件执行超时
	TimeoutString *string `json:"timeout,omitempty"`
	InvokeInterval *time.Duration `json:"-"`
	Timeout *time.Duration `json:"-"`
	// 最大标准输出长度
	MaxOutputLength *int `json:"max_output_length,omitempty"`
	// 并发度
	Concurrency *int `json:"concurrency,omitempty"`
	// 状态消息变更是否导致Condition更新
	EnableMessageChangeBasedConditionUpdate *bool `json:"enable_message_change_based_condition_update,omitempty"`
}

// 此PD的配置，结构上对应配置文件
type CustomPluginConfig struct {
	// PD类型，必须为custom
	Plugin string `json:"plugin,omitempty"`
	// 全局配置
	PluginGlobalConfig pluginGlobalConfig `json:"pluginConfig,omitempty"`
	// 源名称
	Source string `json:"source"`
	// CPM需要处理的所有Condition的默认状态
	DefaultConditions []types.Condition `json:"conditions"`
	// 需要解析和执行的插件列表
	Rules []*CustomRule `json:"rules"`
	// 状态消息变更是否导致Condition更新
	EnableMetricsReporting *bool `json:"metricsReporting,omitempty"`
}</pre>
<p>CPM的核心代码：</p>
<pre class="crayon-plain-tag">package custompluginmonitor

import (
	"encoding/json"
	"io/ioutil"
	"time"

	"github.com/golang/glog"

	"k8s.io/node-problem-detector/pkg/custompluginmonitor/plugin"
	cpmtypes "k8s.io/node-problem-detector/pkg/custompluginmonitor/types"
	"k8s.io/node-problem-detector/pkg/problemdaemon"
	"k8s.io/node-problem-detector/pkg/problemmetrics"
	"k8s.io/node-problem-detector/pkg/types"
	"k8s.io/node-problem-detector/pkg/util"
	"k8s.io/node-problem-detector/pkg/util/tomb"
)

// 此PD的名称
const CustomPluginMonitorName = "custom-plugin-monitor"

func init() {
	problemdaemon.Register(
		CustomPluginMonitorName,
		types.ProblemDaemonHandler{
			CreateProblemDaemonOrDie: NewCustomPluginMonitorOrDie,
			CmdOptionDescription:     "Set to config file paths."})
}

// CPM
type customPluginMonitor struct {
	configPath string
	config     cpmtypes.CustomPluginConfig
	conditions []types.Condition
	// 规则执行插件
	plugin     *plugin.Plugin
	// 插件执行结果的读通道
	resultChan &lt;-chan cpmtypes.Result
	// 向NPM报送状态的写通道
	statusChan chan *types.Status
	tomb       *tomb.Tomb
}

// 工厂函数
func NewCustomPluginMonitorOrDie(configPath string) types.Monitor {
	c := &amp;customPluginMonitor{
		configPath: configPath,
		tomb:       tomb.NewTomb(),
	}
	// 读取并校验配置
	f, err := ioutil.ReadFile(configPath)
	err = json.Unmarshal(f, &amp;c.config)
	// Apply configurations
	err = (&amp;c.config).ApplyConfiguration()
	// Validate configurations
	err = c.config.Validate()
	// 创建插件对象
	c.plugin = plugin.NewPlugin(c.config)
	// 状态通道
	c.statusChan = make(chan *types.Status, 1000)

	if *c.config.EnableMetricsReporting {
		initializeProblemMetricsOrDie(c.config.Rules)
	}
	return c
}

// 初始化问题指标
func initializeProblemMetricsOrDie(rules []*cpmtypes.CustomRule) {
	for _, rule := range rules {
		if rule.Type == types.Perm {
			err := problemmetrics.GlobalProblemMetricsManager.SetProblemGauge(rule.Condition, rule.Reason, false)
		}
		err := problemmetrics.GlobalProblemMetricsManager.IncrementProblemCounter(rule.Reason, 0)
	}
}

// 启动
func (c *customPluginMonitor) Start() (&lt;-chan *types.Status, error) {
	// 启动插件
	go c.plugin.Run()
	// 启动主循环
	go c.monitorLoop()
	return c.statusChan, nil
}

// 停止
func (c *customPluginMonitor) Stop() {
	c.tomb.Stop()
}

// 主循环
func (c *customPluginMonitor) monitorLoop() {
	c.initializeStatus()
	// 得到插件的结果通道
	resultChan := c.plugin.GetResultChan()
	// 循环遍历处理插件的结果
	for {
		select {
		case result := &lt;-resultChan:
			glog.V(3).Infof("Receive new plugin result for %s: %+v", c.configPath, result)
			// 将插件结果转换为Status
			status := c.generateStatus(result)
			glog.Infof("New status generated: %+v", status)
			// 输出到状态通道
			c.statusChan &lt;- status
		case &lt;-c.tomb.Stopping():
			c.plugin.Stop()
			glog.Infof("Custom plugin monitor stopped: %s", c.configPath)
			c.tomb.Done()
			break
		}
	}
}

// 从插件检查结果生成状态
func (c *customPluginMonitor) generateStatus(result cpmtypes.Result) *types.Status {
	timestamp := time.Now()
	var activeProblemEvents []types.Event
	var inactiveProblemEvents []types.Event
	if result.Rule.Type == types.Temp {
		// 对于临时错误，如果插件检查结果非0则产生一个事件
		if result.ExitStatus &gt;= cpmtypes.NonOK {
			activeProblemEvents = append(activeProblemEvents, types.Event{
				Severity:  types.Warn,
				Timestamp: timestamp,
				Reason:    result.Rule.Reason,
				Message:   result.Message,
			})
		}
	} else {
		// 对于永久错误，如果插件检查结果非0则修改Condition
		for i := range c.conditions {
			condition := &amp;c.conditions[i]
			if condition.Type == result.Rule.Condition {
				// 规则中的Reason、结果中的Message表明了发生的问题
				// 需要从配置中读取默认的Condition，以便在检查结果OK时恢复Condition
				var defaultConditionReason string
				var defaultConditionMessage string
				for j := range c.config.DefaultConditions {
					defaultCondition := &amp;c.config.DefaultConditions[j]
					// conditions.type == rules[j].condition
					if defaultCondition.Type == result.Rule.Condition {
						defaultConditionReason = defaultCondition.Reason
						defaultConditionMessage = defaultCondition.Message
						break
					}
				}

				needToUpdateCondition := true
				var newReason string
				var newMessage string
				// 如果检查结果为0则不处于Condition，为1则处于Condition，其它值则未知
				status := toConditionStatus(result.ExitStatus)
				if condition.Status == types.True &amp;&amp; status != types.True {
					// Condtion从True转变为False/Unknown
					newReason = defaultConditionReason
					if newMessage == "" {
						newMessage = defaultConditionMessage
					} else {
						newMessage = result.Message
					}
				} else if condition.Status != types.True &amp;&amp; status == types.True {
					// Condtion从False/Unknown转变为True
					newReason = result.Rule.Reason
					newMessage = result.Message
				} else if condition.Status != status {
					// Condtion在False和Unknown之间转换
					newReason = defaultConditionReason
					if newMessage == "" {
						newMessage = defaultConditionMessage
					} else {
						newMessage = result.Message
					}
				} else if condition.Status == types.True &amp;&amp; status == types.True &amp;&amp;
					(condition.Reason != result.Rule.Reason ||
						(*c.config.PluginGlobalConfig.EnableMessageChangeBasedConditionUpdate &amp;&amp; condition.Message != result.Message)) {
					// Condtion没有改变，和上次一样是True
					newReason = result.Rule.Reason
					newMessage = result.Message
				} else {
					// Condtion没有改变，和上次一样是False/Unknown
					needToUpdateCondition = false
				}
				
				if needToUpdateCondition {
					condition.Transition = timestamp
					condition.Status = status
					condition.Reason = newReason
					condition.Message = newMessage

					updateEvent := util.GenerateConditionChangeEvent(
						condition.Type,
						status,
						newReason,
						timestamp,
					)

					if status == types.True {
						activeProblemEvents = append(activeProblemEvents, updateEvent)
					} else {
						inactiveProblemEvents = append(inactiveProblemEvents, updateEvent)
					}
				}

				break
			}
		}
	}
	// 报告指标
	if *c.config.EnableMetricsReporting {
		for _, event := range activeProblemEvents {
			err := problemmetrics.GlobalProblemMetricsManager.IncrementProblemCounter( event.Reason, 1)
		}
		for _, condition := range c.conditions {
			err := problemmetrics.GlobalProblemMetricsManager.SetProblemGauge(
				condition.Type, condition.Reason, condition.Status == types.True)
		}
	}
	// 发布Status
	return &amp;types.Status{
		Source: c.config.Source,
		Events:     append(activeProblemEvents, inactiveProblemEvents...),
		Conditions: c.conditions,
	}
}

// 将插件退出码转换为Condtion.Status
func toConditionStatus(s cpmtypes.Status) types.ConditionStatus {
	switch s {
	case cpmtypes.OK:
		return types.False
	case cpmtypes.NonOK:
		return types.True
	default:
		return types.Unknown
	}
}

// 初始化，报告默认状态
func (c *customPluginMonitor) initializeStatus() {
	c.conditions = initialConditions(c.config.DefaultConditions)
	glog.Infof("Initialize condition generated: %+v", c.conditions)
	c.statusChan &lt;- &amp;types.Status{
		Source:     c.config.Source,
		Conditions: c.conditions,
	}
}

func initialConditions(defaults []types.Condition) []types.Condition {
	conditions := make([]types.Condition, len(defaults))
	copy(conditions, defaults)
	for i := range conditions {
		conditions[i].Status = types.False
		conditions[i].Transition = time.Now()
	}
	return conditions
}</pre>
<p>实际上执行监控脚本的工作，由Plugin这个结构负责，并且将每个脚本的执行结果通过通道传递给CPM： </p>
<pre class="crayon-plain-tag">package plugin

import (
	"context"
	"fmt"
	"os/exec"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/golang/glog"
	cpmtypes "k8s.io/node-problem-detector/pkg/custompluginmonitor/types"
	"k8s.io/node-problem-detector/pkg/util/tomb"
)

type Plugin struct {
	config     cpmtypes.CustomPluginConfig
	// 此通道用于控制并发度
	syncChan   chan struct{}
	// 此通道用于输出结果
	resultChan chan cpmtypes.Result
	tomb       *tomb.Tomb
	// 嵌入
	sync.WaitGroup
}

func NewPlugin(config cpmtypes.CustomPluginConfig) *Plugin {
	return &amp;Plugin{
		config:   config,
		// 限制通道大小为并发度
		syncChan: make(chan struct{}, *config.PluginGlobalConfig.Concurrency),
		resultChan: make(chan cpmtypes.Result, 1000),
		tomb:       tomb.NewTomb(),
	}
}

func (p *Plugin) GetResultChan() &lt;-chan cpmtypes.Result {
	return p.resultChan
}

// 执行所有规则一遍
func (p *Plugin) Run() {
	defer func() {
		glog.Info("Stopping plugin execution")
		p.tomb.Done()
	}()

	// 仅仅支持全局一致的调度间隔
	runTicker := time.NewTicker(*p.config.PluginGlobalConfig.InvokeInterval)
	defer runTicker.Stop()

	runner := func() {
		glog.Info("Start to run custom plugins")

		for _, rule := range p.config.Rules {
			// 占据一个位置
			p.syncChan &lt;- struct{}{}
			// 增加一个等待量，在synChan基础上又要加等待组的原因是
			// 防止遍历完毕后，Run方法过早退出
			p.Add(1)
			// 异步执行规则
			go func(rule *cpmtypes.CustomRule) {
				// 总是减少一个等待量
				defer p.Done()
				// 同时释放一个位置
				defer func() {
					&lt;-p.syncChan
				}()

				start := time.Now()
				// 执行规则
				exitStatus, message := p.run(*rule)
				end := time.Now()

				glog.V(3).Infof("Rule: %+v. Start time: %v. End time: %v. Duration: %v", rule, start, end, end.Sub(start))
				
				// 写入结果
				result := cpmtypes.Result{
					Rule:       rule,
					ExitStatus: exitStatus,
					Message:    message,
				}

				p.resultChan &lt;- result

				glog.Infof("Add check result %+v for rule %+v", result, rule)
			}(rule)
		}
		// 等待所有规则执行完毕，防止过早退出
		p.Wait()
		glog.Info("Finish running custom plugins")
	}
	// 首次执行
	select {
	case &lt;-p.tomb.Stopping():
		return
	default:
		runner()
	}
	// 循环执行
	for {
		select {
		case &lt;-runTicker.C:
			runner()
		case &lt;-p.tomb.Stopping():
			return
		}
	}
}

// 执行单个规则
func (p *Plugin) run(rule cpmtypes.CustomRule) (exitStatus cpmtypes.Status, output string) {
	var ctx context.Context
	var cancel context.CancelFunc
	// 使用全局、当前规则超时中更小的值
	if rule.Timeout != nil &amp;&amp; *rule.Timeout &lt; *p.config.PluginGlobalConfig.Timeout {
		ctx, cancel = context.WithTimeout(context.Background(), *rule.Timeout)
	} else {
		ctx, cancel = context.WithTimeout(context.Background(), *p.config.PluginGlobalConfig.Timeout)
	}
	defer cancel()
	// 执行系统命令
	cmd := exec.CommandContext(ctx, rule.Path, rule.Args...)
	stdout, err := cmd.Output()
	// 如果出错，且退出码不是1则认为是Unknown
	if err != nil {
		if _, ok := err.(*exec.ExitError); !ok {
			glog.Errorf("Error in running plugin %q: error - %v. output - %q", rule.Path, err, string(stdout))
			return cpmtypes.Unknown, "Error in running plugin. Please check the error log"
		}
	}

	output = string(stdout)
	output = strings.TrimSpace(output)

	// 超时
	if cmd.ProcessState.Sys().(syscall.WaitStatus).Signaled() {
		output = fmt.Sprintf("Timeout when running plugin %q: state - %s. output - %q", rule.Path, cmd.ProcessState.String(), output)
	}

	// 修剪标准输出
	if len(output) &gt; *p.config.PluginGlobalConfig.MaxOutputLength {
		output = output[:*p.config.PluginGlobalConfig.MaxOutputLength]
	}

	exitCode := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
	switch exitCode {
	case 0:
		return cpmtypes.OK, output
	case 1:
		return cpmtypes.NonOK, output
	default:
		return cpmtypes.Unknown, output
	}
}

func (p *Plugin) Stop() {
	p.tomb.Stop()
	glog.Info("Stop plugin execution")
}</pre>
<p>注意CPM这里的Plugin，和NPD配置中提到的Plugin，不是一个概念。</p>
<div class="blog_h1"><span class="graybg">cluster-autoscaler</span></div>
<p>云提供者通常支持动态创建、销毁节点。当NPD检测到故障节点，且无法恢复时，“治愈”措施就是Drain节点。这导致集群规模的缩小，应当调用云提供者的接口，补充节点。</p>
<p>开源项目<a href="https://github.com/kubernetes/autoscaler">autoscaler</a>为K8S提供了若干额外的自动扩容组件：</p>
<ol>
<li>Cluster Autoscaler：能够自动对K8S节点数量进行扩缩，保证所有Pod有地方运行，且自动销毁空闲节点。支持GCP、AWS、Azure、阿里云、百度云，其它云环境需要自行扩展</li>
<li>Vertical Pod Autoscaler：一系列组件，能够自动调整Pod的CPU、内存请求。未来可能支持inplace-update，也就是说不需要删除Pod即可完成request值的修改</li>
<li>Addon Resizer：简化版的VPA，根据集群的节点规模，自动修改Deployment的request值</li>
</ol>
<p>和本文主题相关的是cluster-autoscaler，我们可以利用它来保证集群规模的稳定。</p>
<div class="blog_h1"><span class="graybg">velero<a id="velero"></a></span></div>
<p>在灾难性故障中，K8S集群可能完全无法恢复，只能重建。那么，如何快速重建K8S集群就是关键技术问题。</p>
<p><a href="https://velero.io">Velero</a>（Heptio Ark）是一个能进行K8S集群备份、迁移的开源项目，特性包括：</p>
<ol>
<li>集群备份：支持备份完整集群（的K8S资源以及持久卷），或者根据命名空间、标签选择器来备份集群的一部分</li>
<li>定期备份</li>
<li>备份钩子：在备份之前、之后执行指定的运维操作</li>
<li>迁移：将K8S资源迁移到其它集群，例如将生产环境集群复制到开发环境</li>
</ol>
<p>Velero由两个部分组成：</p>
<ol>
<li>运行在K8S集群中的服务器端</li>
<li>运行在客户机上的CLI</li>
</ol>
<p>你可以在云提供者或者裸金属环境的K8S集群上运行Velero。Velero集成对流行厂商的<a href="https://velero.io/docs/v1.1.0/support-matrix/">存储服务</a>的支持。</p>
<div class="blog_h2"><span class="graybg">存储要求</span></div>
<p>你需要首先选择一个<span style="background-color: #c0c0c0;">对象存储后端，用于存放备份的Etcd数据</span>。Velero支持S3兼容的对象存储，例如Ceph Rados 12.2.7、Minio。</p>
<p>要<span style="background-color: #c0c0c0;">支持持久卷的备份</span>，必须选择一个<a href="https://velero.io/docs/v1.1.0/support-matrix/#volume-snapshot-providers">卷快照提供者</a>，国内仅阿里云支持。裸金属集群要使用持久卷备份功能，可以<a href="https://github.com/heptio/velero-plugin-example/">自行开发插件</a>，或者使用<a href="https://github.com/restic/restic">Restic</a>等通用的存储备份工具，但是性能明显比卷快照差。Restic支持多种存储后端（用来存放它生成的备份），但是<span style="background-color: #c0c0c0;">Velero+Restic仅仅支持S3兼容的对象存储</span>。</p>
<div class="blog_h3"><span class="graybg">提供者列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">提供者</td>
<td style="text-align: center;">对象存储</td>
<td style="text-align: center;">卷Snapshotter</td>
<td style="text-align: center;">插件仓库</td>
</tr>
</thead>
<tbody>
<tr>
<td>AWS</td>
<td>AWS S3</td>
<td>AWS EBS</td>
<td><a href="https://github.com/vmware-tanzu/velero-plugin-for-aws">地址 </a></td>
</tr>
<tr>
<td>GCP</td>
<td>Google Cloud Storage</td>
<td>Google Compute Engine Disks</td>
<td><a href="https://github.com/vmware-tanzu/velero-plugin-for-gcp">地址</a></td>
</tr>
<tr>
<td>Azure</td>
<td>Azure Blob Storage</td>
<td>Azure Managed Disks</td>
<td><a href="https://github.com/vmware-tanzu/velero-plugin-for-microsoft-azure">地址</a></td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Restic</span></div>
<p>Restic是一个开源备份工具，Velero可以与之集成，实现任何类型的K8S持久卷备份。如果你的存储后端没有对应的Velero插件，或者使用了EFS、AzureFile、NFS、emptyDir、Local PV等没有快照概念的卷，可以考虑Restic。<span style="background-color: #c0c0c0;">HostPath不被支持</span>。</p>
<div class="blog_h3"><span class="graybg">自定义资源</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">CR</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ResticRepository</td>
<td>
<p>表示并管理Velero的Restic仓库的生命周期。在第一次针对某个命名空间的备份请求创建后，Velero为每个命名空间创建一个Restic仓库，此CR的控制器会调用Restic仓库的生命周期命令，例如restic init, restic check,  restic prune</p>
<p>调用<pre class="crayon-plain-tag">velero restic repo get</pre>可以获得Velero的Restic仓库的信息</p>
</td>
</tr>
<tr>
<td>PodVolumeBackup</td>
<td>
<p>表示一个Pod中的一个卷的Restic备份，当发现被注解的Pod后，Velero备份主进程会创建一个或多个PodVolumeBackup对象</p>
<p>集群中会运行一个Daemonset，这样每个节点上都会运行一个控制器，负责执行restic backup命令以备份Pod的卷数据</p>
</td>
</tr>
<tr>
<td>PodVolumeRestore</td>
<td>
<p>表示一个Pod中的一个卷的Restic恢复，Velero主恢复进程发现Pod关联了Restic备份后，会创建一个或多个这种CR</p>
<p>同样的，每个节点上运行的控制器负责执行restic restoure恢复本机Pod的卷</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<ol>
<li>要求K8S 1.10以上支持的挂载传播（MountPropagation） 特性，此特性允许同一个Pod之间的容器共享同一个卷</li>
<li>不支持HostPath类型的卷</li>
<li>Restic会加密所有备份数据，目前Velero使用一个静态的、通用的密钥。这意味着潜在的安全风险，所有能够访问你的OSS桶的人都能够解密数据。未来Velero会提供更加完善的安全支持</li>
<li>目前Velero<span style="background-color: #c0c0c0;">基于Pod的名称</span>来关联备份，这意味着Deployment的<span style="background-color: #c0c0c0;">Pod删除重建后，会产生一个全新的，而非增量的备份</span></li>
<li>Restic使用单线程扫描所有文件，如果需要备份的文件很大，例如数据库文件，<span style="background-color: #c0c0c0;">扫描并去重的过程会很慢，即使实际差异很小</span></li>
</ol>
<div class="blog_h3"><span class="graybg">备份</span></div>
<p>要备份K8S资源或卷的内容，您需要使用自定义资源Backup，针对该资源的有效操作是创建、删除，修改没有意义。</p>
<p>关于K8S资源的备份，需要注意以下几点：</p>
<ol>
<li>正在被删除的资源不会包含在备份中</li>
<li>您可以为任何资源添加标签exclude-from-backup，以禁止对它进行备份</li>
<li>在配置Backup资源时，可以通过命名空间、资源类型、标签指定过滤器，不匹配的资源不会包含在备份中</li>
</ol>
<p>关于K8S卷的备份，需要注意以下几点：</p>
<ol>
<li>卷备份基于Restic实现，它的工作方式是找到Pod卷的挂载目录，并将其内容复制出来</li>
<li>卷备份是Pod备份的附加项。而持久卷还有另外一种备份机制，即快照</li>
<li>对于访问模式为ReadWriteMany的持久卷，如果有多个Pod挂载了它，则仅仅会备份一次</li>
</ol>
<p>非命名空间内资源的备份行为，受到includeClusterResources配置影响：</p>
<ol>
<li>true：备份集群级别资源，具体行为受标签选择器、资源类型选择器影响</li>
<li>false：不备份集群级别资源</li>
<li>null/unset：
<ol>
<li>如果备份包含了所有命名空间则备份所有集群级别资源</li>
<li>否则，仅仅当备份包含的命名空间中的资源<span style="background-color: #c0c0c0;">所关联的集群级别资源</span>包含到备份中。例如PersistentVolumeClaim关联的PersistentVolume会包含到备份中</li>
</ol>
</li>
</ol>
<p>备份流程：</p>
<ol>
<li>主备份进程会检查每个它需要备份的Pod上的注解，如果有backup.velero.io/backup-volumes则意味着需要Restic备份</li>
<li>Velero会确保Pod的命名空间的Restic仓库存在：
<ol>
<li>检查ResticRepository对象是否存在</li>
<li>如果不存在，则创建一个新的，并等待ResticRepository控制器初始化、检查</li>
</ol>
</li>
<li>Velero为每个需要备份的卷（列为上述注解的值）创建PodVolumeBackup对象</li>
<li>主备份进程等待PodVolumeBackup完成或失败</li>
<li>与此同时，每个PodVolumeBackup被对应节点的控制器处理，此控制器：
<ol>
<li>具有一个HostPath挂载点，对应宿主机 /var/lib/kubelet/pods目录，以便访问Pod卷数据</li>
<li>在上述HostPath下找到Pod卷的子目录</li>
<li>执行<pre class="crayon-plain-tag">restic backup</pre></li>
<li>更新CR的状态为Completed或Failed</li>
</ol>
</li>
<li>当所有PodVolumeBackup完成后，Velero主进程将这些CR添加到备份中，存放在名为BACKUPNAME-podvolumebackups.json.gz的文件中，并且上传到对象存储，Restic备份的Tar包同样会存放在对象存储中</li>
</ol>
<p>操作步骤：</p>
<ol>
<li>为Pod添加注解，指明哪些卷需要备份：<br />
<pre class="crayon-plain-tag">kubectl -n YOUR_POD_NAMESPACE annotate pod/YOUR_POD_NAME \
    backup.velero.io/backup-volumes=YOUR_VOLUME_NAME_1,YOUR_VOLUME_NAME_2,...</pre>
</li>
<li>然后，创建一个Velero备份CR：<pre class="crayon-plain-tag">velero backup create NAME OPTIONS...</pre> </li>
<li>当备份完成后，查看其信息：<pre class="crayon-plain-tag">velero backup describe YOUR_BACKUP_NAME</pre> </li>
<li>获取卷备份对象：<br />
<pre class="crayon-plain-tag">kubectl -n velero get podvolumebackups -l velero.io/backup-name=YOUR_BACKUP_NAME -o yaml</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">恢复</span></div>
<p>恢复流程：</p>
<ol>
<li>主Velero恢复进程检查所有PodVolumeBackup资源</li>
<li>对于每个PodVolumeBackup，Velero首先保证Restic仓库存在：
<ol>
<li>检查ResticRepository资源是否存在于目标命名空间</li>
<li>如果不存在，则创建之，并且等待ResticRepository控制器完成Restic仓库的初始化和检查。在恢复时，真实的Restic仓库应该已经存在于对象存储中，因此实际上仅仅是检查其完整性</li>
</ol>
</li>
<li>Velero为Pod添加初始化容器，其任务是等待此Pod所有卷恢复完成</li>
<li>Velero将添加了初始化容器的Pod提交给K8S</li>
<li>对于每个需要恢复的卷，创建PodVolumeRestore</li>
<li>Velero主进程等待每个PodVolumeRestore完成或失败</li>
<li>与此同时，每个PodVolumeRestore会被恰当节点上的控制器处理，该控制器：
<ol>
<li>通过HostPath挂载 /var/lib/kubelet/pods，以便访问Pod卷数据</li>
<li>等待Pod运行Init容器</li>
<li>找到Init容器的卷子目录，这些卷和主容器是共享的</li>
<li>运行<pre class="crayon-plain-tag">restic restore</pre></li>
<li>如果恢复成，则在卷的.velero子目录中写入一个文件，文件名为当前Velero Restore的UID</li>
<li>更新PodVolumeRestore的状态为Completed或Failed</li>
</ol>
</li>
<li>初始化容器等待，直到发现<span style="background-color: #c0c0c0;">所有相关的卷的根目录下的.velero内有文件写入</span>，其UID为本次Restore的UID。初始化容器退出，主容器开始运行</li>
</ol>
<p>在执行Restic恢复时，Velero使用一个助手init容器。其镜像默认为gcr.io/heptio-images/velero-restic-restore-helper:VERSION，其中VERSION和Velero的版本一致。如果需要使用定制的镜像，可以在Velero的命名空间创建一个ConfigMap：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  # 名称无所谓，基于标签找到此ConfigMap
  name: restic-restore-action-config
  namespace: velero
  labels:
    # 下面的标签用于识别此ConfigMap是某个插件的配置信息
    velero.io/plugin-config: ""
    # 下面的标签说明插件的名称和类型
    velero.io/restic: RestoreItemAction
data:
  image: myregistry.io/my-custom-helper-image[:OPTIONAL_TAG]
  cpuRequest: 200m
  memRequest: 128Mi
  cpuLimit: 200m
  memLimit: 128Mi</pre>
<div class="blog_h2"><span class="graybg">安装</span></div>
<div class="blog_h3"><span class="graybg">通过命令行</span></div>
<p>首先到<a href="https://github.com/heptio/velero/releases/tag/v1.1.0">https://github.com/heptio/velero/releases/tag/v1.1.0</a>下载最新版本的客户端，解压放到$PATH下。</p>
<p>然后，使用如下命令安装：</p>
<pre class="crayon-plain-tag">velero install \
    # 备份和卷存储的提供者名称
    --provider &lt;YOUR_PROVIDER&gt; \
    # 对象存储桶的名字
    --bucket &lt;YOUR_BUCKET&gt; \
    # Velero的IAM帐户的凭证文件，如果不支持，使用 --no-secret 
    --secret-file &lt;PATH_TO_FILE&gt; \
    --velero-pod-cpu-request &lt;CPU_REQUEST&gt; \
    --velero-pod-mem-request &lt;MEMORY_REQUEST&gt; \
    --velero-pod-cpu-limit &lt;CPU_LIMIT&gt; \
    --velero-pod-mem-limit &lt;MEMORY_LIMIT&gt; \
    # 启用Restic集成
    [--use-restic] \
    [--restic-pod-cpu-request &lt;CPU_REQUEST&gt;] \
    [--restic-pod-mem-request &lt;MEMORY_REQUEST&gt;] \
    [--restic-pod-cpu-limit &lt;CPU_LIMIT&gt;] \
    [--restic-pod-mem-limit &lt;MEMORY_LIMIT&gt;]</pre>
<p>下面是一个基于MinIO、不支持存储卷快照的例子：</p>
<pre class="crayon-plain-tag">velero install \
    --provider aws \
    --bucket velero \
    --secret-file ./credentials-velero \
    --use-volume-snapshots=false \
    --backup-location-config region=minio,s3ForcePathStyle="true",s3Url=https://minio.k8s.gmem.cc \
    # 启用Restic，等待部署完成
    --use-restic --wait</pre>
<p>其中密钥文件的格式如下：</p>
<pre class="crayon-plain-tag">[default]
aws_access_key_id = minio
aws_secret_access_key = minio123</pre>
<p>要卸载Velero时，删除以下K8S资源即可：</p>
<pre class="crayon-plain-tag">kubectl delete namespace/velero clusterrolebinding/velero
kubectl delete crds -l component=velero</pre>
<div class="blog_h3"><span class="graybg">通过Helm</span></div>
<pre class="crayon-plain-tag">kubectl create ns velero
kubectl -n velero create sa velero-server
kubectl get secrets gmemregsecret -o yaml --export | kubectl -n velero create -f -


helm install --namespace velero --name velero --set fullnameOverride=velero \
    --set configuration.provider=aws \
    --set-file credentials.secretContents.cloud=./credentials-velero \
    --set configuration.backupStorageLocation.name=aws \
    --set configuration.backupStorageLocation.bucket=velero \
    --set configuration.backupStorageLocation.config.region=minio \
    --set configuration.backupStorageLocation.config.s3ForcePathStyle=true \
    --set configuration.backupStorageLocation.config.s3Url=https://minio.k8s.gmem.cc \
    --set image.repository=docker.gmem.cc/velero/velero \
    --set image.tag=v1.1.0 \
    --set image.pullPolicy=IfNotPresent \
    --set serviceAccount.server.name=velero-server \
    --set serviceAccount.server.create=true \
    --set snapshotsEnabled=false \
    --set deployRestic=true \
    velero 

kubectl -n velero patch sa velero-server -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}'</pre>
<p>要删除，执行：</p>
<pre class="crayon-plain-tag">helm delete velero --purge

kubectl delete crd backups.velero.io                  
kubectl delete crd backupstoragelocations.velero.io   
kubectl delete crd deletebackuprequests.velero.io     
kubectl delete crd downloadrequests.velero.io         
kubectl delete crd podvolumebackups.velero.io         
kubectl delete crd podvolumerestores.velero.io        
kubectl delete crd resticrepositories.velero.io       
kubectl delete crd restores.velero.io                 
kubectl delete crd schedules.velero.io                
kubectl delete crd serverstatusrequests.velero.io     
kubectl delete crd volumesnapshotlocations.velero.io</pre>
<div class="blog_h3"><span class="graybg">Restic集成</span></div>
<p>即使在安装时没有启用Restic集成，后续你仍然可以随时调用<pre class="crayon-plain-tag">velero install --use-restic</pre>启用Restic集成。</p>
<div class="blog_h3"><span class="graybg">资源限制</span></div>
<p>Velero可能产生两个Deployment，一个是Velero控制器，一个是Restic，前文包含定制它们的资源用量的参数</p>
<div class="blog_h3"><span class="graybg">多个对象存储位置</span></div>
<p>你可以为备份、卷快照指定多个存储位置。但是velero install时最多指定一个备份存储位置、一个卷快照存储位置。</p>
<p>后续你可以使用命令<pre class="crayon-plain-tag">velero backup-location create</pre>、<pre class="crayon-plain-tag">velero snapshot-location create</pre>添加新的存储位置。</p>
<p>如果在安装阶段不想提供默认的备份存储位置，可以指定<pre class="crayon-plain-tag">--no-default-backup-location</pre>，同时不指定--bucket、--provider。</p>
<p>对象存储位置映射为自定义资源BackupStorageLocation。它对应了一个桶，所有Velero数据都会存放在此桶的某个前缀下。一些供应商特定的字段（例如AWS区域、Azure存储帐户）也存放在此CR中。</p>
<p>用户可以预先配置多个对象存储位置、卷快照存储位置，并且<span style="background-color: #c0c0c0;">在创建备份的时候选择使用哪个位置</span>。</p>
<div class="blog_h3"><span class="graybg">额外的卷快照位置</span></div>
<p>Velero支持通过插件方式来集成不同的卷快照提供者，你可以用AWS S3作为对象存储，而Portworx作为卷快照。</p>
<p>但是velero install仅仅支持配置单个提供者，同时用于对象存储、卷快照。</p>
<p>为了使用不同的卷快照提供者，你需要：</p>
<ol>
<li>指定合理的对象存储参数，安装Velero服务器组件</li>
<li>将卷快照提供者插件添加到Velero</li>
<li>添加卷快照位置<br />
<pre class="crayon-plain-tag">velero snapshot-location create &lt;NAME&gt; --provider &lt;PROVIDER-NAME&gt; [--config &lt;PROVIDER-CONFIG&gt;] </pre>
</li>
</ol>
<p>快照存储位置映射为自定义资源VolumeSnapshotLocation，其字段完全取决于具体供应商（例如AWS区域、Azure资源组、Portworx快照类型）。</p>
<div class="blog_h2"><span class="graybg">使用</span></div>
<div class="blog_h3"><span class="graybg">备份</span></div>
<pre class="crayon-plain-tag"># 安装一个K8S应用
kubectl apply -f examples/nginx-app/base.yaml

# 备份指定命名空间
velero backup create nginx-backup --include-namespaces nginx-example

# 使用选择器，仅仅备份匹配标签的对象
velero backup create nginx-backup --selector app=nginx
# 反向选择器
velero backup create nginx-backup --selector 'backup notin (ignore)'


# 查看备份
velero backup get nginx-backup</pre>
<div class="blog_h3"><span class="graybg">定期备份</span></div>
<pre class="crayon-plain-tag"># 使用Cron表达式
velero schedule create nginx-daily --schedule="0 1 * * *" --selector app=nginx
# 每天
velero schedule create nginx-daily --schedule="@daily" --selector app=nginx </pre>
<div class="blog_h3"><span class="graybg">恢复 </span></div>
<pre class="crayon-plain-tag"># 模拟灾难
kubectl delete namespaces nginx-example
# 动态分配的PV的默认回收策略是Delete，因此上述命令会导致Nginx的PV的后被存储被删除，注意此删除
# 是异步的，因此，执行下一步之前，手工确认卷已经被删除

# 恢复
velero restore create --from-backup nginx-backup

# 查看恢复状态
velero restore get</pre>
<div class="blog_h3"><span class="graybg">清理</span></div>
<pre class="crayon-plain-tag"># 删除备份，包括对象存储、持久卷快照
velero backup delete BACKUP_NAME</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/problem-detection-and-auto-repairing-in-k8s">Kubernetes故障检测和自愈</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/problem-detection-and-auto-repairing-in-k8s/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>OpenAPI学习笔记</title>
		<link>https://blog.gmem.cc/openapi</link>
		<comments>https://blog.gmem.cc/openapi#comments</comments>
		<pubDate>Fri, 12 Jul 2019 09:08:45 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Work]]></category>
		<category><![CDATA[OpenAPI]]></category>
		<category><![CDATA[Swagger]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=28075</guid>
		<description><![CDATA[<p>简介 OpenAPI是一套API规范（ OpenAPI Specification ，OAS），用于定义RESTful API的接口。OpenAPI最初来自SmartBear的Swagger规范。 OpenAPI 目前的版本是3.0，当前Swagger和OpenAPI的关系是： OpenAPI是一套规范 Swagger是实现OpenAPI规范的工具集，包括： Swagger Editor：允许你使用YAML语言在浏览器中编写规范，并实时查看生成的API文档 Swagger UI：从OAS兼容的API动态生成一套Web的美观的API文档 Swagger Codegen：用于生成OpenAPI的客户端库（SDK）、服务器桩代码、文档 Swagger Parser：从Java解析Open API定义的独立库 Swagger Core：一个Java库，用于创建、消费、使用OpenAPI定义 Swagger <a class="read-more" href="https://blog.gmem.cc/openapi">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/openapi">OpenAPI学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>OpenAPI是一套API规范（ OpenAPI Specification ，OAS），用于定义RESTful API的接口。OpenAPI<span style="background-color: #c0c0c0;">最初来自SmartBear的Swagger规范</span>。</p>
<p>OpenAPI 目前的版本是3.0，当前Swagger和OpenAPI的关系是：</p>
<ol>
<li>OpenAPI是一套规范</li>
<li>Swagger是实现OpenAPI规范的工具集，包括：
<ol>
<li>Swagger Editor：允许你使用YAML语言在浏览器中编写规范，并实时查看生成的API文档</li>
<li>Swagger UI：从OAS兼容的API动态生成一套Web的美观的API文档</li>
<li>Swagger Codegen：用于生成OpenAPI的客户端库（SDK）、服务器桩代码、文档</li>
<li>Swagger Parser：从Java解析Open API定义的独立库</li>
<li>Swagger Core：一个Java库，用于创建、消费、使用OpenAPI定义</li>
<li>Swagger Inspector：从现有的API生成OpenAPI定义、验证API的测试工具</li>
<li>SwaggerHub：OpenAPI的API设计、文档平台</li>
</ol>
</li>
</ol>
<p>Swagger并非唯一支持OpenAPI的工具，到<a href="https://github.com/OAI/OpenAPI-Specification/blob/master/IMPLEMENTATIONS.md">OpenAPI-Specification</a>的GitHub页面可以看到相关工具的列表。</p>
<div class="blog_h1"><span class="graybg">Swagger 2.0</span></div>
<div class="blog_h2"><span class="graybg">API示例</span></div>
<pre class="crayon-plain-tag">{
  // 基本信息
  "swagger": "2.0",
  "info": {
    "title": "Kubernetes",
    "version": "v1.12.1"
  },
  // API端点列表
  "paths": {
    // 端点
    "/api/": {
      // 操作
      "get": {
        "description": "get available API versions",
        // 支持的MIME类型
        "consumes": [
          "application/json",
          "application/yaml",
          "application/vnd.kubernetes.protobuf"
        ],
        "produces": [
          "application/json",
          "application/yaml",
          "application/vnd.kubernetes.protobuf"
        ],
        // 支持的协议
        "schemes": [
          "https"
        ],
        "tags": [
          "core"
        ],
        // 操作的唯一标识
        "operationId": "getCoreAPIVersions",
        // 请求参数说明
        "parameters": [
          {
            "type": "string",
            "description": "Username ",
            "name": "username",
            // 这是URL路径变量
            "in": "path",
            "required": true
          },
          {
            "name": "user",
            // 这是请求体参数，通常是映射到模型的JSON
            "in": "body",
            "required": true,
            "schema": {
              "type": "object",
              "$ref": "#/definitions/models.User"
            }
          },
          {
            "type": "integer",
            "name": "size",
            // 这是请求参数
            "in": "query"
          }
        ],
        // 响应说明，每个状态码对应一个元素
        "responses": {
          "200": {
            "description": "OK",
            // 响应的Schema
            "schema": {
              "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.APIVersions"
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    }
  }
  // Schema定义列表
  "definitions": {
    // Schema名以域名倒写形式
    "io.k8s.apimachinery.pkg.apis.meta.v1.APIVersions": {
      "description": "APIVersions lists the versions that are available, to allow clients to discover the API at /api, which is the root path of the legacy v1 API.",
      // 必须属性
      "required": [
        "versions",
        "serverAddressByClientCIDRs"
      ],
      // 属性规格列表
      "properties": {
        "apiVersion": {
          "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
          "type": "string"
        },
        "kind": {
          "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
          "type": "string"
        },
        "serverAddressByClientCIDRs": {
          "description": "a map of client CIDR to server address that is serving this group. This is to help clients reach servers in the most network-efficient way possible. Clients can use the appropriate server address as per the CIDR that they match. In case of multiple matches, clients should use the longest matching CIDR. The server returns only those CIDRs that it thinks that the client can match. For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.",
          "type": "array",
          "items": {
            "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ServerAddressByClientCIDR"
          }
        },
        "versions": {
          "description": "versions are the api versions that are available.",
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      },
      // K8S扩展字段
      "x-kubernetes-group-version-kind": [
        {
          "group": "",
          "kind": "APIVersions",
          "version": "v1"
        }
      ]
    }
  }
}</pre>
<div class="blog_h2"><span class="graybg">转换为OAS3.0</span></div>
<div class="blog_h3"><span class="graybg">swagger2openapi</span></div>
<p>这是一个Node.js开发的工具，可以将Swagger 2.0转换为OAS 3.0，执行下面的命令安装：</p>
<pre class="crayon-plain-tag">npm install -g swagger2openapi</pre>
<p>调用格式：<pre class="crayon-plain-tag">swagger2openapi source-spec.json [options]</pre></p>
<p>常用选项说明：</p>
<pre class="crayon-plain-tag">--resolveInternal    # 是否解析内部引用
--warnProperty       # 警告扩展配置，默认x-s2o-warning
-e, --encoding       # 输入输出编码，默认utf8
-f, --fatal          # 如果引用解析失败，则终止转换
-i, --indent         # JSON缩进，默认4
-o, --outfile        # 输出到文件而非标准输出
-p, --patch          # 尝试修复源定义中的错误
-r, --resolve        # 是否解析外部引用
-t, --targetVersion  # OAS版本，默认3.0.0
-u, --url            # 源的URL
-w, --warnOnly       # 遇到不可修复错误时发出警告而非报错
-y, --yaml           # 输出为YAML格式而非JSON</pre>
<div class="blog_h2"><span class="graybg">API文档生成</span></div>
<p>通过某些工具，可以从既有代码中生成Swagger API文档。</p>
<div class="blog_h3"><span class="graybg">swag</span></div>
<p><a href="https://github.com/swaggo/swag">该工具</a>能够将Go Annotations转换为Swagger 2.0文档，为多种流行的Go Web框架（例如Gin）提供了插件，从而快速和既有Web项目集成。</p>
<p>执行下面的命令安装到GOPATH下：</p>
<pre class="crayon-plain-tag">go get -u github.com/swaggo/swag/cmd/swag</pre>
<p>在项目根目录，使用<pre class="crayon-plain-tag">swag init</pre>命令可以解析Go代码中的注解并且生成docs目录、docs/docs.go。你需要提供General API annotations，如果这些注解没有存放在根目录的main.go文件中，需要用<pre class="crayon-plain-tag">-g</pre>来指定Go文件路径：</p>
<pre class="crayon-plain-tag">swag init -g pkg/route/routers.go</pre>
<p>使用swag init命令之后，你需要导入生成的docs包以及swaggo的另外两个包：</p>
<pre class="crayon-plain-tag">_ "github.com/gmemcc/myproject/docs"
import "github.com/swaggo/gin-swagger" // gin-swagger middleware
import "github.com/swaggo/files" // swagger embed files</pre>
<p>General API annotations可用字段：</p>
<pre class="crayon-plain-tag">// @title Swagger Example API
// @version 1.0
// @description This is a sample server celler server.
// @termsOfService http://swagger.io/terms/

// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io

// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html

// @host localhost:8080
// @BasePath /api/v1
// @query.collection.format multi

// @securityDefinitions.basic BasicAuth

// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization

// @securitydefinitions.oauth2.application OAuth2Application
// @tokenUrl https://example.com/oauth/token
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information

// @securitydefinitions.oauth2.implicit OAuth2Implicit
// @authorizationurl https://example.com/oauth/authorize
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information

// @securitydefinitions.oauth2.password OAuth2Password
// @tokenUrl https://example.com/oauth/token
// @scope.read Grants read access
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information

// @securitydefinitions.oauth2.accessCode OAuth2AccessCode
// @tokenUrl https://example.com/oauth/token
// @authorizationurl https://example.com/oauth/authorize
// @scope.admin Grants read and write access to administrative information

// @x-extension-openapi {"example": "value on a json format"}</pre>
<p>示例：</p>
<pre class="crayon-plain-tag">// @BasePath /myproject/apis/v2
// @version 2.0.0
// @title Myproject API
// @description my project
// @contact.name alex
// @contact.email myproject@gmem.cc
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html</pre>
<p>swag init生成的docs包，导出了变量<pre class="crayon-plain-tag">SwaggerInfo</pre>，通过此变量你可以编程式的设置各种字段（和上面这种注释方式等效）：</p>
<pre class="crayon-plain-tag">package main

import (
	"github.com/gin-gonic/gin"
	"github.com/swaggo/files"
	"github.com/swaggo/gin-swagger"
	
	"./docs" // docs is generated by Swag CLI, you have to import it.
)

func main() {
	// programmatically set swagger info
	docs.SwaggerInfo.Title = "Swagger Example API"
	docs.SwaggerInfo.Description = "This is a sample server Petstore server."
	docs.SwaggerInfo.Version = "1.0"
	docs.SwaggerInfo.Host = "petstore.swagger.io"
	docs.SwaggerInfo.BasePath = "/v2"
	docs.SwaggerInfo.Schemes = []string{"http", "https"}</pre>
<p>为了在当前Web服务（的HTTP服务器）中查看API文档，需要注册路由，以gin为例： </p>
<pre class="crayon-plain-tag">r := gin.New()

	// use ginSwagger middleware to serve the API docs
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

	r.Run()
}</pre>
<p>要生成API，你需要在控制器代码中添加 API Operation annotations。这些注解需要加在controller代码中，所谓controller代码，就是包含所有路由处理函数的包，以gin为例：</p>
<pre class="crayon-plain-tag">package controller

import (
	"fmt"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/swaggo/swag/example/celler/httputil"
	"github.com/swaggo/swag/example/celler/model"
)

// 下面就是一份API Operation annotations
// ShowAccount godoc
// @Summary Show a account
// @Description get string by ID
// @ID get-string-by-int
// @Accept  json
// @Produce  json
// @Param id path int true "Account ID"
// @Success 200 {object} model.Account
// @Header 200 {string} Token "qwerty"
// @Failure 400,404 {object} httputil.HTTPError
// @Failure 500 {object} httputil.HTTPError
// @Failure default {object} httputil.DefaultError
// 这个仅仅影响生成的Swagger API文档，你还需要调用gin的接口，注册下面这个处理函数（控制器）的路由
// @Router /accounts/{id} [get]
func (c *Controller) ShowAccount(ctx *gin.Context) {
	id := ctx.Param("id")
	aid, err := strconv.Atoi(id)
	if err != nil {
		httputil.NewError(ctx, http.StatusBadRequest, err)
		return
	}
	account, err := model.AccountOne(aid)
	if err != nil {
		httputil.NewError(ctx, http.StatusNotFound, err)
		return
	}
	ctx.JSON(http.StatusOK, account)
}</pre>
<p>这些注解仅仅影响生成的Swagger API文档，不会对Web服务的逻辑产生任何影响。</p>
<p>运行Web服务，可以在http://localhost:port/swagger/index.html查看Swagger 2.0 API文档。访问/swagger/doc.json可以获得JSON格式的API</p>
<div class="blog_h1"><span class="graybg">OAS</span></div>
<p>OAS是REST API的描述格式，在一个OpenAPI文件中，你可以定义完整的接口规格，包括：</p>
<ol>
<li>可用API端点列表，针对每个端点允许的操作</li>
<li>操作的输入、输出参数</li>
<li>身份验证方法</li>
<li>附属信息，包括使用条款、License</li>
</ol>
<p>关于OAS的编写，需要注意：</p>
<ol>
<li>可以用YAML、JSON两种格式编写</li>
<li>所有关键字都是大小写敏感的</li>
</ol>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># OpenAPI的版本，可用版本  3.0.0, 3.0.1, 3.0.2
openapi: 3.0.0
info:
  title: '标题'
  description: '描述'
  version: '此OAS的版本，示例0.1.9'

# 提供此API的服务器地址列表
servers:
  - url: http://api.example.com/v1
    description: Optional server description, e.g. Main (production) server
  - url: http://staging-api.example.com
    description: Optional server description, e.g. Internal staging server for testing

# API 端点列表
paths:
  # 端点 /users
  /users:
    # 端点/users的GET方法
    get:
      # 描述性信息
      summary: Returns a list of users.
      description: Optional extended description in CommonMark or HTML.
      # 响应规格说明
      responses:
        # 200响应说明
        '200':    # status code
          description: A JSON array of user names
          # 200响应是JSON格式
          content:
            application/json:
              # JSON的Schema
              schema: 
                # 数组类型
                type: array
                items: 
                  type: string
    # 端点/users的POST方法
    post:
      summary: Creates a user.
      # 请求体
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username:
                  type: string
      responses: 
        '201':
          description: Created
  # 端点/user/{userId}的POST方法
  # {} 中的是路径变量
  /user/{userId}:
    get:
      summary: Returns a user by ID.
      # 参数说明
      parameters:
        - name: userId
          # 这是一个路径变量
          in: path
          required: true
          description: The ID of the user to return.
          # Schema中可以包含字段的验证规则
          schema:
            type: integer
            format: int64
            minimum: 1
      responses:
        '200':
          description: A user object.
          content:
            application/json:
              schema:
                # 对象类型
                type: object
                # 包含属性声明
                properties:
                  id:
                    type: integer
                    format: int64
                    example: 4
                  name:
                    type: string
                    example: Jessica Smith
        # 异常处理
        '400':
          description: The specified user ID is invalid (not a number).
        '404':
          description: A user with the specified ID was not found.
        default:
          description: Unexpected error</pre>
<div class="blog_h2"><span class="graybg">Schema定义和引用</span></div>
<pre class="crayon-plain-tag">components:
  # 定义了一个名为User的Schema
  schemas:
    User:
      properties:
        id:
          type: integer
        name:
          type: string
      # User包含两个属性，都是必须属性
      required:  
        - id
        - name</pre>
<p>下面的API引用上述Schema：</p>
<pre class="crayon-plain-tag">paths:
  /users/{userId}:
    get:
      summary: Returns a user by ID.
      parameters:
        - in: path
          name: userId
          required: true
          type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                # 引用Schema
                $ref: '#/components/schemas/User'</pre>
<div class="blog_h2"><span class="graybg">API服务器和Base URL</span></div>
<p>OAS中的所有API端点，相对于Base URL，假设Base URL为https://api.example.com/v1则端点/users的访问路径为https://api.example.com/v1/users。</p>
<p>Open API 3.0允许在servers数组中声明多个Base URL。</p>
<div class="blog_h3"><span class="graybg">服务器URL模板</span></div>
<p>API服务器的URL中，可以包含变量：</p>
<pre class="crayon-plain-tag">- url: https://{customerId}.saas-app.com:{port}/v2
    variables:
      customerId:
        default: demo
        description: Customer ID assigned by the service provider
      port:
        enum:
          - '443'
          - '8443'
        default: '443'</pre>
<div class="blog_h3"><span class="graybg">覆盖服务器URL</span></div>
<p>你可以在路径级别，甚至操作（方法） 级别，覆盖servers数组：</p>
<pre class="crayon-plain-tag">/ping:
    get:
      servers:
        - url: https://echo.example.com
          description: Override base path for the GET /ping operation</pre>
<div class="blog_h3"><span class="graybg">服务器相对URL </span></div>
<p>servers数组中的URL可以是相对URL，这种情况下，URL相对于提供OpenAPI定义的Web服务器。</p>
<p>举例来说，如果Open API定义存放在http://localhost:3001/openapi.yaml，则servers元素/v2解析为http://localhost:3001/v2 </p>
<div class="blog_h2"><span class="graybg">媒体类型</span></div>
<p>请求或者响应的媒体类型，在content元素下声明：</p>
<pre class="crayon-plain-tag"># 200响应，媒体类型为JSON
paths:
  /employees:
    get:
      summary: Returns a list of employees.
      responses:
        '200':      # Response
          description: OK
          content:  # Response body
            application/json:  # Media type
              schema:          # Must-have
                type: object   # Data type</pre>
<div class="blog_h3"><span class="graybg">多种媒体类型</span></div>
<p>可以指定多种媒体类型：</p>
<pre class="crayon-plain-tag">paths:
  /employees:
    get:
      responses:
        '200':
          description: OK
          content:
            # JSON
            application/json:
             schema: 
               $ref: '#/components/schemas/Employee'
            # XML
            application/xml:
             schema: 
               $ref: '#/components/schemas/Employee'</pre>
<div class="blog_h3"><span class="graybg">通配媒体类型 </span></div>
<p>如果需要为多种媒体类型指定相同的Schema，可以使用<pre class="crayon-plain-tag">*/*</pre>或者<pre class="crayon-plain-tag">application/*</pre>这样的通配符： </p>
<pre class="crayon-plain-tag">paths:
  /info/logo:
    get:
      responses:
        '200':
          description: OK
          content:
            image/*:
             schema: 
               type: string
               format: binary</pre>
<div class="blog_h2"><span class="graybg">路径和操作</span></div>
<p>在OAS中，路径即端点（资源），操作即HTTP方法。</p>
<div class="blog_h3"><span class="graybg">路径</span></div>
<p>路径可以包括路径变量，例如：</p>
<pre class="crayon-plain-tag">/users/{id}
/organizations/{orgId}/members/{memberId}
/report.{format}</pre>
<p>路径的description支持多行文本，还允许使用Markdown语法。</p>
<div class="blog_h3"><span class="graybg">操作</span></div>
<p>OpenAPI 3.0支持的HTTP方法包括：get, post, put, patch, delete, head, options,  trace。每个路径可以支持多个操作。</p>
<p>OpenAPI 3.0支持通过路径、查询字符串、请求头、Cookie传递参数。对于POST/PUT/PATCH请求，还可以通过请求体传递数据。</p>
<div class="blog_h3"><span class="graybg">查询字符串参数</span></div>
<p>这种参数不得定义在路径中，下面是错误的用法：</p>
<pre class="crayon-plain-tag">paths:
  /users?role={role}:</pre>
<p>下面则是正确的用法：</p>
<pre class="crayon-plain-tag">paths:
  /users:
    get:
      parameters:
        - in: query
          name: role
          schema:
            type: string
            enum: [user, poweruser, admin]
          required: true</pre>
<div class="blog_h3"><span class="graybg">operationId</span></div>
<p>你可以为操作指定一个标识符：</p>
<pre class="crayon-plain-tag">/users:
  get:
    operationId: getUsers</pre>
<p>此标识符必须在OAS中是唯一的。 operationId的使用场景包括：</p>
<ol>
<li>某些代码生成器使用operationId生成对应的方法名</li>
</ol>
<div class="blog_h2"><span class="graybg">描述参数</span></div>
<p>参数需要定义在operation或path的parameters字段下。每个参数包括名字、位置、数据类型（通过schema或content定义）等属性。</p>
<div class="blog_h3"><span class="graybg">参数位置</span></div>
<p>参数可以通过以下HTTP元素传递：</p>
<ol>
<li>路径参数</li>
<li>查询参数</li>
<li>请求头参数</li>
<li>Cookie参数 </li>
</ol>
<div class="blog_h3"><span class="graybg">路径参数</span></div>
<pre class="crayon-plain-tag">/users/{id}:
    get:
      parameters:
        - in: path
          # 必须和路径变量{}中的一样
          name: id   
          required: true
          schema:
            type: integer
            minimum: 1
          description: The user ID</pre>
<p>路径参数可以是数组，或者对象。这种参数在URL中的串行化形式可以是：</p>
<ol>
<li>路径风格展开，分号分隔，示例：<pre class="crayon-plain-tag">/map/point;x=50;y=20</pre></li>
<li>标签展开，点号前缀，示例：<pre class="crayon-plain-tag">/color.R=100.G=200.B=150</pre></li>
<li>简单形式，逗号分隔，示例：<pre class="crayon-plain-tag">/users/12,34,56</pre></li>
</ol>
<p>具体使用哪种串行化风格，通过style、explode关键字指定。</p>
<div class="blog_h3"><span class="graybg">查询参数</span></div>
<p>这是最常见的参数形式，出现在URL的?后面。</p>
<pre class="crayon-plain-tag">parameters:
        - in: query
          name: offset
          schema:
            type: integer
        - in: query
          name: limit
          schema:
            type: integer</pre>
<p>RFC 3986规定<pre class="crayon-plain-tag">:/?#[]@!$&amp;'()*+,;=</pre>为URI特殊字符。如果查询参数中包含这些字符，必须以%HEX形式编码。</p>
<div class="blog_h3"><span class="graybg">请求头参数</span></div>
<pre class="crayon-plain-tag">paths:
  /ping:
    get:
      summary: Checks if the server is alive
      parameters:
        - in: header
          name: X-Request-ID
          schema:
            type: string
            format: uuid
          required: true</pre>
<div class="blog_h3"><span class="graybg">Cookie参数 </span></div>
<pre class="crayon-plain-tag">parameters:
        - in: cookie
          name: debug
          schema:
            type: integer
            enum: [0, 1]
            default: 0
        - in: cookie
          name: csrftoken
          schema:
            type: string</pre>
<div class="blog_h3"><span class="graybg">参数默认值</span></div>
<p>使用default关键字可以为optional参数提供默认值：</p>
<pre class="crayon-plain-tag">parameters:
  - in: query
    name: offset
    schema:
      type: integer
      minimum: 0
      default: 0
    required: false </pre>
<div class="blog_h3"><span class="graybg">schema和content</span></div>
<p>要描述复杂参数的内容，可以使用schema或者content。这两个关键字是互斥的，用于不同的场景下。</p>
<p>大部分情况下，可以考虑使用schema，使用schema可以描述原始类型、串行化为字符串的对象、简单数组。对象、数组的串行化方法在style、explode关键字中定义：</p>
<pre class="crayon-plain-tag">- in: query
  name: color
  schema:
    type: array
    items:
      type: string
  # 串行化为 color=blue,black,brown 形式
  style: form
  explode: false</pre>
<p>style、explode无法满足的复杂串行化需求，可以使用content：</p>
<pre class="crayon-plain-tag">parameters:
  - in: query
    name: filter
    content:
      # 细粒度的控制如何串行化为JSON
      application/json:
        schema:
          type: object
          properties:
            type:
              type: string
            color:
              type: string</pre>
<div class="blog_h3"><span class="graybg">枚举类型参数</span></div>
<p>可以为某个参数指定可选值的集合，这些值的类型必须和参数类型匹配：</p>
<pre class="crayon-plain-tag">parameters:
  - in: query
    name: status
    schema:
      type: string
      enum:
        - available
        - pending
        - sold</pre>
<p><span class="graybg">常量参数 —— 仅仅包含一个枚举值的参数</span>：</p>
<pre class="crayon-plain-tag">parameters:
  - in: query
    name: rel_date
    required: true
    schema:
      type: string
      enum:
        - now</pre>
<div class="blog_h3"><span class="graybg">可空参数</span></div>
<p>OpenAPI 3.0允许仅仅有名字，而没有值的参数，在URL中表现为<pre class="crayon-plain-tag">/path?parm</pre>的形式：</p>
<pre class="crayon-plain-tag">parameters:
  - in: query
    name: metadata
    schema:
      type: boolean
    allowEmptyValue: true</pre>
<p>你也可以在schema中声明nullable属性：</p>
<pre class="crayon-plain-tag">schema:
  type: integer
  format: int32
  nullable: true</pre>
<div class="blog_h3"><span class="graybg">废弃参数 </span></div>
<p>可以将一个参数标记为已经废弃：</p>
<pre class="crayon-plain-tag">- in: query
  name: format
  required: true
  schema:
    type: string
    enum: [json, xml, yaml]
  deprecated: true</pre>
<div class="blog_h3"><span class="graybg">公共参数</span></div>
<p>所谓公共参数，即一个path下，所有方法都使用的参数定义：</p>
<pre class="crayon-plain-tag">/user/{id}:
  parameters:
    - in: path
      name: id
      schema:
        type: integer
      required: true
      description: The user ID
  get:
    ...
  patch:
    ...
  delete:</pre>
<div class="blog_h2"><span class="graybg">身份验证和授权</span></div>
<p>OpenAPI支持以多种身份验证方式保护API：</p>
<ol>
<li>基于Authorization头的身份认证：
<ol>
<li>不记名令牌（Bearer）</li>
<li>基本认证</li>
</ol>
</li>
<li>请求头、Cookie、查询字符串中的API Key</li>
<li>OAuth2</li>
<li>OpenID连接发现</li>
</ol>
<div class="blog_h3"><span class="graybg">定义securitySchemes</span></div>
<pre class="crayon-plain-tag">securitySchemes:

  BasicAuth:
    type: http
    scheme: basic

  BearerAuth:
    type: http
    scheme: bearer

  ApiKeyAuth:
    type: apiKey
    in: header
    name: X-API-Key

  OpenID:
    type: openIdConnect
    openIdConnectUrl: https://example.com/.well-known/openid-configuration

  OAuth2:
    type: oauth2
    flows:
      authorizationCode:
        authorizationUrl: https://example.com/oauth/authorize
        tokenUrl: https://example.com/oauth/token
        scopes:
          read: Grants read access
          write: Grants write access
          admin: Grants access to admin operations</pre>
<div class="blog_h3"><span class="graybg">应用到方法</span></div>
<p>为方法添加security字段：</p>
<pre class="crayon-plain-tag">security:
  - ApiKeyAuth: []
  - OAuth2:
      - read
      - write </pre>
<div class="blog_h2"><span class="graybg">引用</span></div>
<p>引用Schema：</p>
<pre class="crayon-plain-tag">schema: 
  $ref: '#/components/schemas/User'</pre>
<p>本地引用：<pre class="crayon-plain-tag">#/components/schemas/user</pre></p>
<p>远程引用：</p>
<ol>
<li>当前服务器下其它文件：<pre class="crayon-plain-tag">$ref: 'document.json'</pre></li>
<li>文件中的元素：<pre class="crayon-plain-tag">$ref: 'document.json#/myElement'</pre></li>
<li>其它服务器中的元素：<pre class="crayon-plain-tag">$ref:  http://path/to/your/resource.json#myElement</pre></li>
</ol>
<div class="blog_h1"><span class="graybg">代码生成</span></div>
<div class="blog_h2"><span class="graybg">openapi-generator</span></div>
<p><a href="https://github.com/OpenAPITools/openapi-generator">OpenAPITools/openapi-generator</a>项目，用于从OpenAPI Spec（2和3版本）自动生成客户端库、服务器代码存根、文档以及配置文件。</p>
<p>openapi-generator广泛的支持各种常用语言和框架，对于Go来说，支持的Web框架包括Gin、Echo等。</p>
<div class="blog_h2"><span class="graybg">Go</span></div>
<div class="blog_h3"><span class="graybg">deepmap/oapi-codegen</span></div>
<p>用于生成OpenAPI 3.0的Go样板代码，以Echo为默认的Web框架。该库致力于尽量的简单化，而非通用化，它不会为所有OpenAPI Schemas生成强类型的Go代码。</p>
<p>默认情况下oapi-codegen会生成包括客户端、服务器、类型定义、内嵌Swagger Spec在内的所有代码。对应命令行选项<br /><pre class="crayon-plain-tag">-generate=types,client,server,spec</pre>。</p>
<p>下面我们看看从宠物商店的OpenAPI Spec生成Go样板代码：</p>
<pre class="crayon-plain-tag">openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification
  termsOfService: http://swagger.io/terms/
  contact:
    name: Swagger API Team
    email: apiteam@swagger.io
    url: http://swagger.io
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
  - url: http://petstore.swagger.io/api
paths:
  /pets:
    get:
      description: |
        Returns all pets from the system that the user has access to
        Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
        Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
      operationId: findPets
      parameters:
        - name: tags
          in: query
          description: tags to filter by
          required: false
          style: form
          schema:
            type: array
            items:
              type: string
        - name: limit
          in: query
          description: maximum number of results to return
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: pet response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      description: Creates a new pet in the store. Duplicates are allowed
      operationId: addPet
      requestBody:
        description: Pet to add to the store
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPet'
      responses:
        '200':
          description: pet response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /pets/{id}:
    get:
      description: Returns a user based on a single ID, if the user does not have access to the pet
      operationId: find pet by id
      parameters:
        - name: id
          in: path
          description: ID of pet to fetch
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: pet response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      description: deletes a single pet based on the ID supplied
      operationId: deletePet
      parameters:
        - name: id
          in: path
          description: ID of pet to delete
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '204':
          description: pet deleted
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Pet:
      allOf:
        - $ref: '#/components/schemas/NewPet'
        - type: object
          required:
          - id
          properties:
            id:
              type: integer
              format: int64

    NewPet:
      type: object
      required:
        - name  
      properties:
        name:
          type: string
        tag:
          type: string    

    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string</pre>
<p>通过下面的命令生成样板代码：</p>
<pre class="crayon-plain-tag">go get github.com/deepmap/oapi-codegen/cmd/oapi-codegen
oapi-codegen petstore-expanded.yaml  &gt; petstore.gen.go</pre>
<p> OpenAPI的<pre class="crayon-plain-tag">/components/schemas</pre>段中定义了可复用对象类型，oapi-codegen会生成对应的Go结构：</p>
<pre class="crayon-plain-tag">// Type definition for component schema "Error"
type Error struct {
    Code    int32  `json:"code"`
    Message string `json:"message"`
}

// Type definition for component schema "NewPet"
type NewPet struct {
    Name string  `json:"name"`
    Tag  *string `json:"tag,omitempty"`
}

// Type definition for component schema "Pet"
type Pet struct {
    // Embedded struct due to allOf(#/components/schemas/NewPet)
    NewPet
    // Embedded fields due to inline allOf schema
    Id int64 `json:"id"`
}

// Type definition for component schema "Pets"
type Pets []Pet</pre>
<p>对于在handler中定义的inline类型，只会生成内联的、匿名的Go结构。这会导致重复的代码，应该避免。</p>
<p>对于paths中的每一个元素，都会生成一个Go Handler函数：</p>
<pre class="crayon-plain-tag">type ServerInterface interface {
    //  (GET /pets)
    FindPets(ctx echo.Context, params FindPetsParams) error
    //  (POST /pets)
    AddPet(ctx echo.Context) error
    //  (DELETE /pets/{id})
    DeletePet(ctx echo.Context, id int64) error
    //  (GET /pets/{id})
    FindPetById(ctx echo.Context, id int64) error
}</pre>
<p>请求参数通过以下方式传递：</p>
<ol>
<li>大部分情况下，函数被编解码到echo.Context中</li>
<li>路径变量作为Handler函数的参数</li>
<li>其它请求头参数、查询参数、Cookie参数被存放在params变量中，例如：<br />
<pre class="crayon-plain-tag">// Parameters object for FindPets
type FindPetsParams struct {
   Tags  *[]string `json:"tags,omitempty"`
   Limit *int32   `json:"limit,omitempty"`  // 可选参数，作为指针
}</pre>
</li>
</ol>
<p>使用命令行选项<pre class="crayon-plain-tag">-generate server</pre>可以为Echo生成Handlers注册函数： </p>
<pre class="crayon-plain-tag">func RegisterHandlers(router codegen.EchoRouter, si ServerInterface) {
    wrapper := ServerInterfaceWrapper{
        Handler: si,
    }
    router.GET("/pets", wrapper.FindPets)
    router.POST("/pets", wrapper.AddPet)
    router.DELETE("/pets/:id", wrapper.DeletePet)
    router.GET("/pets/:id", wrapper.FindPetById)
}</pre>
<p>使用下面的代码注册Handlers到Echo服务器：</p>
<pre class="crayon-plain-tag">func SetupHandler() {
    var myApi PetStoreImpl  // 这是你的服务器端实现
    e := echo.New()
    petstore.RegisterHandlers(e, &amp;myApi)
    ...
}</pre>
<p>类似的，使用命令行选项<pre class="crayon-plain-tag">-generate chi-server</pre>可以生成Chi或net/http的Handlers注册函数。</p>
<p>OpenAPI默认隐含additionalProperties=true，也就是说请求中提供的任何没有明确定义的字段都应该被接受。由于在Go语言中处理这种动态属性需要大量样板代码，oapi-codegen默认假设additionalProperties=false，要改变此默认行为你需要修改OpenAPI Schema：</p>
<pre class="crayon-plain-tag">NewPet:
      required:
        - name
      properties:
        name:
          type: string
        tag:
          type: string
      additionalProperties:
        type: string</pre>
<p>这样会生成如下结构：</p>
<pre class="crayon-plain-tag">// NewPet defines model for NewPet.
type NewPet struct {
	Name                 string            `json:"name"`
	Tag                  *string           `json:"tag,omitempty"`
	AdditionalProperties map[string]string `json:"-"`
}</pre>
<p>使用命令行选项<pre class="crayon-plain-tag">-generate=client</pre>可以生成客户端代码：</p>
<pre class="crayon-plain-tag">// The interface specification for the client above.
type ClientInterface interface {

	// FindPets request
	FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) (*http.Response, error)

	// AddPet request with JSON body
	AddPet(ctx context.Context, body NewPet, reqEditors ...RequestEditorFn) (*http.Response, error)

	// DeletePet request
	DeletePet(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error)

	// FindPetById request
	FindPetById(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error)
}


// Client which conforms to the OpenAPI3 specification for this service.
type Client struct {
    // The endpoint of the server conforming to this interface, with scheme,
    // https://api.deepmap.com for example.
    Server string

    // HTTP client with any customized settings, such as certificate chains.
    Client http.Client

    // A callback for modifying requests which are generated before sending over
    // the network.
    RequestEditors []func(ctx context.Context, req *http.Request) error
}</pre>
<p>每个OpenAPI Schema中定义的操作都对应了一个客户端函数。 </p>
<div class="blog_h3"><span class="graybg"><a id="openapi-gen"></a>openapi-gen</span></div>
<p>此工具专门用于从K8S模型类生成Open API模型（以Go结构的形式存放在GetOpenAPIDefinitions函数中）。</p>
<p>下载并安装：</p>
<pre class="crayon-plain-tag">git clone https://github.com/kubernetes/kube-openapi.git

export GOPROXY=https://goproxy.io
export GO111MODULE=on
go install cmd/openapi-gen/openapi-gen.go</pre>
<p>要为指定的包生成模型，执行命令：</p>
<pre class="crayon-plain-tag"># 输入包的导入路径
openapi-gen -i git.pacloud.io/pks/helm-operator/pkg/apis/pks/v1 
            # 输出包的导入路径
            -p git.pacloud.io/pks/helm-operator/pkg/apis/pks/v1 
            # 生成的函数所在文件的名称前缀
            -O zz_generated.openapi</pre>
<p>你的CRD通常会引用K8S核心库中的模型，因此需要同时为它们生成模型：</p>
<pre class="crayon-plain-tag">openapi-gen -i k8s.io/api/core/v1 -p git.pacloud.io/pks/helm-operator/pkg/apis/core/v1
openapi-gen -i k8s.io/apimachinery/pkg/apis/meta/v1 -p git.pacloud.io/pks/helm-operator/pkg/apis/meta/v1</pre>
<p>使用Operator Framework开发CRD的控制器时，你可以使用注释<pre class="crayon-plain-tag">+k8s:openapi-gen=true</pre>来生成Open API模型，生成的Schema示例如下：</p>
<div class="blog_h3"><span class="graybg">生成Swagger Spec</span></div>
<p>下面的代码调用GetOpenAPIDefinitions函数，生成Swagger 2.0.0 API定义的JSON：</p>
<pre class="crayon-plain-tag">package main

import (
	"encoding/json"
	"fmt"
	pksv1 "git.pacloud.io/pks/helm-operator/pkg/apis/pks/v1"
	"github.com/go-openapi/spec"
	"k8s.io/kube-openapi/pkg/common"
	"log"
	"os"
	"strings"
)

func main() {
	if len(os.Args) &lt;= 1 {
		log.Fatal("version required")
	}
	version := os.Args[1]
	if !strings.HasPrefix(version, "v") {
		version = "v" + version
	}
	oAPIDefs := pksv1.GetOpenAPIDefinitions(func(name string) spec.Ref {
		return spec.MustCreateRef("#/definitions/" + common.EscapeJsonPointer(swaggify(name)))
	})
	defs := spec.Definitions{}
	for defName, val := range oAPIDefs {
		defs[swaggify(defName)] = val.Schema
	}

	swagger := spec.Swagger{
		SwaggerProps: spec.SwaggerProps{
			Swagger:     "2.0",
			Definitions: defs,
			Paths:       &amp;spec.Paths{Paths: map[string]spec.PathItem{}},
			Info: &amp;spec.Info{
				InfoProps: spec.InfoProps{
					Title:   "Helm Release",
					Version: version,
				},
			},
		},
	}
	jsonBytes, err := json.MarshalIndent(swagger, "", "  ")
	if err != nil {
		log.Fatal(err.Error())
	}
	fmt.Println(string(jsonBytes))
}

func swaggify(name string) string {
	name = strings.Replace(name, "git.pacloud.io/pks/helm-operator/pkg/apis", "yun.gmem.cc", -1)
	parts := strings.Split(name, "/")
	hostParts := strings.Split(parts[0], ".")
	for i, j := 0, len(hostParts)-1; i &lt; j; i, j = i+1, j-1 {
		hostParts[i], hostParts[j] = hostParts[j], hostParts[i]
	}
	parts[0] = strings.Join(hostParts, ".")
	return strings.Join(parts, ".")
}</pre>
<p>生成的Swagger API，仅仅包含模型信息，也就是definitions字段，但是不包含API端点（paths）。</p>
<p>如果需要生成API端点，则需要启动一个API Server并将需要生成API端点的模型注册进去：</p>
<pre class="crayon-plain-tag">// k8s.io/*包的API非常不稳定，本样例基于kubernetes 1.13.9
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	pkscorev1 "git.pacloud.io/pks/helm-operator/pkg/apis/core/v1"
	pksmetav1 "git.pacloud.io/pks/helm-operator/pkg/apis/meta/v1"
	pksv1 "git.pacloud.io/pks/helm-operator/pkg/apis/pks/v1"
	"github.com/go-openapi/spec"
	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	"k8s.io/apimachinery/pkg/watch"
	openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
	"k8s.io/apiserver/pkg/registry/rest"
	genericapiserver "k8s.io/apiserver/pkg/server"
	genericoptions "k8s.io/apiserver/pkg/server/options"
	"k8s.io/klog"
	"k8s.io/kube-openapi/pkg/builder"
	"k8s.io/kube-openapi/pkg/common"
	"net"
	"os"
)

// 这个Storage是资源CRUD操作的提供者
type StandardStorage struct {
	cfg ResourceInfo
}

// 强制它实现以下接口
var _ rest.GroupVersionKindProvider = &amp;StandardStorage{}
var _ rest.Scoper = &amp;StandardStorage{}
var _ rest.StandardStorage = &amp;StandardStorage{}

// GroupVersionKindProvider
func (r *StandardStorage) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind {
	return r.cfg.gvk
}

// Scoper
func (r *StandardStorage) NamespaceScoped() bool {
	return r.cfg.namespaceScoped
}

// Getter
func (r *StandardStorage) New() runtime.Object {
	return r.cfg.obj
}

func (r *StandardStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
	return r.New(), nil
}

func (r *StandardStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
	return r.New(), nil
}

// Lister
func (r *StandardStorage) NewList() runtime.Object {
	return r.cfg.list
}

func (r *StandardStorage) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
	return r.NewList(), nil
}

// CreaterUpdater
func (r *StandardStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
	return r.New(), true, nil
}

// GracefulDeleter
func (r *StandardStorage) Delete(ctx context.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
	return r.New(), true, nil
}

// CollectionDeleter
func (r *StandardStorage) DeleteCollection(ctx context.Context, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
	return r.NewList(), nil
}

// Watcher
func (r *StandardStorage) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
	return nil, nil
}

type ResourceInfo struct {
	gvk             schema.GroupVersionKind
	obj             runtime.Object
	list            runtime.Object
	namespaceScoped bool
}

type TypeInfo struct {
	GroupVersion    schema.GroupVersion
	Resource        string
	Kind            string
	NamespaceScoped bool
}
type Config struct {
	Scheme             *runtime.Scheme
	Codecs             serializer.CodecFactory
	Info               spec.InfoProps
	OpenAPIDefinitions []common.GetOpenAPIDefinitions
	Resources          []TypeInfo
}

func (c *Config) GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
	out := map[string]common.OpenAPIDefinition{}
	for _, def := range c.OpenAPIDefinitions {
		for k, v := range def(ref) {
			out[k] = v
		}
	}
	return out
}
func RenderOpenAPISpec(cfg Config) (string, error) {

	// 总是需要向Scheme注册corev1、metav1
	metav1.AddToGroupVersion(cfg.Scheme, schema.GroupVersion{Version: "v1"})
	unversioned := schema.GroupVersion{Group: "", Version: "v1"}
	cfg.Scheme.AddUnversionedTypes(unversioned,
		&amp;metav1.Status{},
		&amp;metav1.APIVersions{},
		&amp;metav1.APIGroupList{},
		&amp;metav1.APIGroup{},
		&amp;metav1.APIResourceList{},
	)

	// API Server选项
	options := genericoptions.NewRecommendedOptions("/registry/pks.yun.gmem.cc", cfg.Codecs.LegacyCodec(), &amp;genericoptions.ProcessInfo{})
	options.SecureServing.BindPort = 6445
	options.Etcd = nil
	options.Authentication = nil
	options.Authorization = nil
	options.CoreAPI = nil
	options.Admission = nil
	// 自动生成的服务器证书的存放目录
	options.SecureServing.ServerCert.CertDirectory = "/tmp/helm-operator"

	// 启动的API Server，监听的地址
	publicAddr := "localhost"
	ips := []net.IP{net.ParseIP("127.0.0.1")}

	// 尝试自动生成服务器证书
	if err := options.SecureServing.MaybeDefaultWithSelfSignedCerts(publicAddr, nil, ips); err != nil {
		klog.Fatal(err)
	}

	// API Server配置
	serverConfig := genericapiserver.NewRecommendedConfig(cfg.Codecs)
	if err := options.ApplyTo(serverConfig, cfg.Scheme); err != nil {
		return "", err
	}
	serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(cfg.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(cfg.Scheme))
	serverConfig.OpenAPIConfig.Info.InfoProps = cfg.Info
	//                             完成配置，  创建服务器
	genericServer, err := serverConfig.Complete().New("helm-operator-server", genericapiserver.NewEmptyDelegate())
	if err != nil {
		return "", err
	}

	// 这里处理本服务器需要Serve的资源列表
	table := map[schema.GroupVersion]map[string]rest.Storage{}
	{
		for _, ti := range cfg.Resources {
			// 对于每一种资源，根据其GV寻找存储库
			var resmap map[string]rest.Storage
			if m, found := table[ti.GroupVersion]; found {
				resmap = m
			} else {
				// 如果找不到，则创建存储库（每个GV一个存储库）
				resmap = map[string]rest.Storage{}
				table[ti.GroupVersion] = resmap
			}

			gvk := ti.GroupVersion.WithKind(ti.Kind)
			// 创建这种资源的一个对象
			obj, err := cfg.Scheme.New(gvk)
			if err != nil {
				return "", err
			}
			// 创建这种资源的列表对象
			list, err := cfg.Scheme.New(ti.GroupVersion.WithKind(ti.Kind + "List"))
			if err != nil {
				return "", err
			}

			// 为资源创建存储，并Put到它的GV的存储库中
			resmap[ti.Resource] = &amp;StandardStorage{ResourceInfo{
				// GVK信息
				gvk: gvk,
				// 资源和资源列表的原型
				obj:  obj,
				list: list,
				// 提示此资源是否命名空间化
				namespaceScoped: ti.NamespaceScoped,
			}}
		}
	}
	for gv, resmap := range table {
		// 为每个组创建API组信息
		apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(gv.Group, cfg.Scheme, metav1.ParameterCodec, cfg.Codecs)
		storage := map[string]rest.Storage{}
		for r, s := range resmap {
			storage[r] = s
		}
		apiGroupInfo.VersionedResourcesStorageMap[gv.Version] = storage

		// 并安装到此服务器
		if err := genericServer.InstallAPIGroup(&amp;apiGroupInfo); err != nil {
			return "", err
		}
	}
	// 构件API规范，也就是Swagger 2.0 Spec
	spec, err := builder.BuildOpenAPISpec(genericServer.Handler.GoRestfulContainer.RegisteredWebServices(), serverConfig.OpenAPIConfig)
	if err != nil {
		return "", err
	}
	data, err := json.MarshalIndent(spec, "", "  ")
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func main() {
	flag.Parse()
	if len(os.Args) &lt;= 1 {
		panic("version required")
	}
	version := os.Args[1]

	scheme := runtime.NewScheme()
	// 将我们需要处理的类型加入到scheme
	scheme.AddKnownTypeWithName(schema.GroupVersion{Group: "pks.yun.gmem.cc", Version: "v1"}.WithKind("Release"), &amp;pksv1.Release{})
	scheme.AddKnownTypeWithName(schema.GroupVersion{Group: "pks.yun.gmem.cc", Version: "v1"}.WithKind("ReleaseList"), &amp;pksv1.Release{})
	scheme.AddKnownTypeWithName(schema.GroupVersion{Group: "pks.yun.gmem.cc", Version: "v1"}.WithKind("WatchEvent"), &amp;pksv1.Release{})

	spec, err := RenderOpenAPISpec(Config{
		Info: spec.InfoProps{
			Version: version,
			Title:   "Helm Operator OpenAPI",
		},
		Scheme: scheme,
		Codecs: serializer.NewCodecFactory(scheme),
		OpenAPIDefinitions: []common.GetOpenAPIDefinitions{
			pkscorev1.GetOpenAPIDefinitions,
			pksmetav1.GetOpenAPIDefinitions,
			pksv1.GetOpenAPIDefinitions,
		},
		Resources: []TypeInfo{
			{
				GroupVersion:    schema.GroupVersion{Group: "pks.yun.gmem.cc", Version: "v1"},
				Kind:            "Release",
				Resource:        "Release",
				NamespaceScoped: true,
			},
		},
	})
	if err != nil {
		klog.Fatal(err.Error())
	}
	fmt.Println(spec)
}</pre>
<div class="blog_h2"><span class="graybg">Java</span></div>
<div class="blog_h3"><span class="graybg">Gradle</span></div>
<p>可以使用下面的插件：</p>
<pre class="crayon-plain-tag">plugins {
  id 'org.hidetake.swagger.generator' version '2.18.1'
}</pre>
<p>根据API规格的版本，选择依赖：</p>
<pre class="crayon-plain-tag">dependencies {
  swaggerCodegen 'io.swagger:swagger-codegen-cli:2.4.2'             // Swagger Codegen V2
  swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.5'  // Swagger Codegen V3
  swaggerCodegen 'org.openapitools:openapi-generator-cli:3.3.4'     // penAPI Generator
}</pre>
<p>控制代码生成的配置片段：</p>
<pre class="crayon-plain-tag">swaggerSources {
  petstore {
    inputFile = file('petstore.yaml')
    code {
      language = 'spring'
      configFile = file('config.json')
    }
  }
}</pre>
<p>执行命令：<pre class="crayon-plain-tag">./gradlew generateSwaggerCode</pre>，将自动生成代码到<pre class="crayon-plain-tag">build/swagger-code-petstore</pre>目录。</p>
<p>configFile字段可以指定一个外部（本质上就是<pre class="crayon-plain-tag">swagger-codegen-cli generate</pre>的）配置文件，格式如下：</p>
<pre class="crayon-plain-tag">{
  "library": "spring-mvc",
  "modelPackage": "example.model",
  "apiPackage": "example.api",
  "invokerPackage": "example"
}</pre>
<p>对于language=java，常用配置项列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>sortParamsByRequiredFlag</td>
<td>排序风发参数，将必须参数放在前面</td>
</tr>
<tr>
<td>ensureUniqueParams</td>
<td>是否一个操作内，保证参数名的唯一性</td>
</tr>
<tr>
<td>allowUnicodeIdentifiers</td>
<td>是否允许Unicode标识符</td>
</tr>
<tr>
<td>modelPackage</td>
<td>生成的模型类的包名</td>
</tr>
<tr>
<td>apiPackage</td>
<td>生成的API类的包名</td>
</tr>
<tr>
<td>invokerPackage</td>
<td>生成的代码的根包名</td>
</tr>
<tr>
<td>groupId</td>
<td rowspan="4">生成的POM的信息</td>
</tr>
<tr>
<td>artifactId</td>
</tr>
<tr>
<td>artifactVersion</td>
</tr>
<tr>
<td>artifactDescription</td>
</tr>
<tr>
<td>sourceFolder</td>
<td>源码目录</td>
</tr>
<tr>
<td>localVariablePrefix</td>
<td>局部变量前缀</td>
</tr>
<tr>
<td>serializableModel</td>
<td>生成的模型是否实现Serializable接口</td>
</tr>
<tr>
<td>fullJavaUtil</td>
<td>如果生成Java代码，那么是否用全限定名称引用java.util下的类</td>
</tr>
<tr>
<td>withXml</td>
<td>生成的类是否添加序列化为XML的支持</td>
</tr>
<tr>
<td>dateLibrary</td>
<td>
<p>Java日期库：</p>
<p style="padding-left: 30px;">joda 用于遗留应用<br />legacy 使用java.util.Date，用于遗留应用<br />java8-localdatetime   使用LocalDateTime，用于遗留应用<br />java8 使用Java 8原生的JSR310<br />threetenbp  低版本的JSR310垫片</p>
</td>
</tr>
<tr>
<td>java8</td>
<td>使用Java 8提供的API</td>
</tr>
<tr>
<td>disableHtmlEscaping</td>
<td>使用JSON时禁止HTML转译，如果<span style="background-color: #c0c0c0;">使用byte[]字段应该设置为true</span></td>
</tr>
<tr>
<td>useGzipFeature</td>
<td>是否使用Gzip编码请求</td>
</tr>
<tr>
<td>useRuntimeException</td>
<td>是否使用RuntimeException代替Exception</td>
</tr>
<tr>
<td>library</td>
<td>
<p>库模板，对于language=java，默认okhttp-gson。可选：</p>
<p style="padding-left: 30px;">jersey1，基于JacksonJSON串行化<br />jersey2，基于JacksonJSON串行化<br />feign，基于JacksonJSON串行化<br />okhttp-gson，基于GSON串行化<br />retrofit2 基于OKHttp 3.x，基于GSON串行化<br />resttemplate 基于Spring RestTemplate，基于JacksonJSON串行化</p>
</td>
</tr>
</tbody>
</table>
<p>  </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/openapi">OpenAPI学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/openapi/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Gin学习笔记</title>
		<link>https://blog.gmem.cc/gin-study-note</link>
		<comments>https://blog.gmem.cc/gin-study-note#comments</comments>
		<pubDate>Thu, 23 May 2019 15:05:19 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=27229</guid>
		<description><![CDATA[<p>简介 Gin是一个Go语言Web框架，使用类似于martini（另一个Web框架）风格的API，但是（基于httprouter）大大提升了性能。 主要优势： 基于基数树（Radix Tree）的路由，内存占用小，性能高 中间件支持，请求可以被中间件链条+最后的动作（Action）处理，中间件可以完成日志记录、身份校验、GZIP压缩等操作 不会崩溃，能够自动检测到Panic并恢复 支持请求的JSON校验 路由组支持，可以更好的组织API，例如需要/不需要身份验证、不同的API版本。路由组可以嵌套 错误管理，提供一个便利的方式收集错误信息 内置对JSON、XML格式API支持，以及HTML渲染支持 可扩展性，扩展自己的中间件很方便 快速起步 安装 [crayon-69d3148a67903473783534/] 构建 Gin默认使用encoding/json作为JSON解析库，如果你希望使用性能更好的jsoniter，可以： [crayon-69d3148a67908442767316/] 使用 引入如下包即可使用Gin： [crayon-69d3148a6790b876644602/] 如果你想使用[crayon-69d3148a6790d018612450-i/]之类的常量，需要引入[crayon-69d3148a6790f311757358-i/] <a class="read-more" href="https://blog.gmem.cc/gin-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/gin-study-note">Gin学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Gin是一个Go语言Web框架，使用类似于martini（另一个Web框架）风格的API，但是（基于httprouter）大大提升了性能。</p>
<p>主要优势：</p>
<ol>
<li>基于基数树（Radix Tree）的路由，内存占用小，性能高</li>
<li>中间件支持，请求可以被中间件链条+最后的动作（Action）处理，中间件可以完成日志记录、身份校验、GZIP压缩等操作</li>
<li>不会崩溃，能够自动检测到Panic并恢复</li>
<li>支持请求的JSON校验</li>
<li>路由组支持，可以更好的组织API，例如需要/不需要身份验证、不同的API版本。路由组可以嵌套</li>
<li>错误管理，提供一个便利的方式收集错误信息</li>
<li>内置对JSON、XML格式API支持，以及HTML渲染支持</li>
<li>可扩展性，扩展自己的中间件很方便</li>
</ol>
<div class="blog_h1"><span class="graybg">快速起步</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">go get -u github.com/gin-gonic/gin</pre>
<div class="blog_h2"><span class="graybg">构建</span></div>
<p>Gin默认使用encoding/json作为JSON解析库，如果你希望使用性能更好的jsoniter，可以：</p>
<pre class="crayon-plain-tag">go build -tags=jsoniter .</pre>
<div class="blog_h2"><span class="graybg">使用</span></div>
<p>引入如下包即可使用Gin：</p>
<pre class="crayon-plain-tag">import "github.com/gin-gonic/gin"</pre>
<p>如果你想使用<pre class="crayon-plain-tag">http.StatusOK</pre>之类的常量，需要引入<pre class="crayon-plain-tag">import "net/http"</pre></p>
<div class="blog_h2"><span class="graybg">第一个服务</span></div>
<pre class="crayon-plain-tag">package main

import "github.com/gin-gonic/gin"

func main() {
	// 返回默认的引擎实例，该实例启用了Logger、Recovery两个中间件
	r := gin.Default()
	// 路由规则：/ping 由该函数处理
	r.GET("/ping", func(c *gin.Context) {
		//  写入一个JSON消息
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	// 默认在0.0.0.0:8080监听
	r.Run()
}</pre>
<div class="blog_h1"><span class="graybg">示例</span></div>
<div class="blog_h2"><span class="graybg">优雅启停</span></div>
<pre class="crayon-plain-tag">package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	// 将路由器注册为HTTP服务器的Handler
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "Welcome Gin Server")
	})

	srv := &amp;http.Server{
		Addr:    ":8080",
		Handler: router,
	}
	// 在新线程中启动HTTP服务器
	go func() {
		if err := srv.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	// 主线程等待信号
	quit := make(chan os.Signal)
	// kill    命令发送 syscanll.SIGTERM
	// kill -2 命令发送 syscall.SIGINT
	// kill -9 发送 SIGKILL，但是无法处理，因此不添加
	//                  当出现以下信号时，写入quit通道
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	// 等到通道可读
	&lt;-quit
	log.Println("Shutdown Server ...")

	// 优雅停止，最多等待5秒
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	//        停止服务器
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server Shutdown:", err)
	}
	// 记录超时事件
	select {
	case &lt;-ctx.Done():
		log.Println("timeout of 5 seconds.")
	}
	log.Println("Server exiting")
}</pre>
<div class="blog_h2"><span class="graybg">定制HTTP服务器</span></div>
<div class="blog_h3"><span class="graybg">定制参数</span></div>
<pre class="crayon-plain-tag">func main() {
	router := gin.Default()
	// Gin路由器实现了标准的Handler接口 
	s := &amp;http.Server{
		Addr:           ":8080",
		Handler:        router,
		ReadTimeout:    10 * time.Second,
		WriteTimeout:   10 * time.Second,
		MaxHeaderBytes: 1 &lt;&lt; 20,
	}
	s.ListenAndServe()
}</pre>
<div class="blog_h3"><span class="graybg">HTTPS支持</span></div>
<pre class="crayon-plain-tag">r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key") </pre>
<div class="blog_h2"><span class="graybg">自定义中间件</span></div>
<div class="blog_h3"><span class="graybg">简单例子</span></div>
<pre class="crayon-plain-tag">// 自定义中间件
func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()

		// 设置一个变量
		c.Set("example", "12345")

		// 请求处理之前

		c.Next()

		// 请求处理之后
		latency := time.Since(t)
		log.Print(latency)

		// 访问Action设置的状态码
		status := c.Writer.Status()
		log.Println(status)
	}
}

func main() {
	r := gin.New()
	// 使用中间件
	r.Use(Logger())

	r.GET("/test", func(c *gin.Context) {
		// 可以访问中间件设置的变量
		example := c.MustGet("example").(string)
		log.Println(example)
	})
	r.Run(":8080")
} </pre>
<div class="blog_h3"><span class="graybg">打印请求和响应</span></div>
<p>需要对相关对象进行装饰：</p>
<pre class="crayon-plain-tag">type loggingWriter struct {
	gin.ResponseWriter
	respBody *bytes.Buffer
}
// 装饰Response Writer，写出响应的同时记录到日志缓冲
func (lw *loggingWriter) Write(data []byte) (int, error) {
	lw.respBody.Write(data)
	return lw.ResponseWriter.Write(data)
}


router.Use(func(c *gin.Context) {
	reqBody, _ := ioutil.ReadAll(c.Request.Body)
	// 装饰请求体，忽略关闭请求
	c.Request.Body = ioutil.NopCloser(bytes.NewReader(reqBody))
	lw := loggingWriter{
		ResponseWriter: c.Writer,
		respBody:       bytes.NewBuffer([]byte{}),
	}
	// 更换Response Writer
	c.Writer = &amp;lw
	c.Next()
	log.Debug(fmt.Sprintf("%s request to %s with request body %s responsed %d with response body %s",
		c.Request.Method, c.Request.URL, reqBody, lw.Status(), lw.respBody))
}) </pre>
<div class="blog_h3"><span class="graybg">中间件中的Goroutine</span></div>
<p>如果你在中间件中启动新的Goroutine，则你不应该使用原始的Gin上下文，必须使用它的副本：</p>
<pre class="crayon-plain-tag">func main() {
	r := gin.Default()
	// 长异步操作
	r.GET("/long_async", func(c *gin.Context) {
		// 获得Goroutine内使用的副本
		cCp := c.Copy()
		go func() {
			// 模拟耗时操作
			time.Sleep(5 * time.Second)
			// 通过副本获得请求上下文信息
			log.Println("Done! in path " + cCp.Request.URL.Path)
		}()
	})

	r.GET("/long_sync", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		// 同步操作不需要副本
		log.Println("Done! in path " + c.Request.URL.Path)
	})

	r.Run(":8080")
} </pre>
<div class="blog_h2"><span class="graybg">AsciiJSON </span></div>
<p>支持生成对非ASCII字符进行转义的JSON：</p>
<pre class="crayon-plain-tag">func main() {
	r := gin.Default()

	r.GET("/someJSON", func(c *gin.Context) {
		data := map[string]interface{}{
			"lang": "GO语言",
			"tag":  "&lt;br&gt;",
		}

		// will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
		c.AsciiJSON(http.StatusOK, data)
	})
	r.Run(":8080")
}</pre>
<div class="blog_h2"><span class="graybg">日志配置</span></div>
<div class="blog_h3"><span class="graybg">定制日志格式</span></div>
<pre class="crayon-plain-tag">func main() {
	router := gin.New()
	// 使用中间件
	// 中间件LoggerWithFormatter默认将日志写入到gin.DefaultWriter，后者默认为os.Stdout
	router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
		// 定制日志格式
		return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
				param.ClientIP,
				param.TimeStamp.Format(time.RFC1123),
				param.Method,
				param.Path,
				param.Request.Proto,
				param.StatusCode,
				param.Latency,
				param.Request.UserAgent(),
				param.ErrorMessage,
		)
	}))
	// 使用中间件
	router.Use(gin.Recovery())
	router.GET("/ping", func(c *gin.Context) {
		c.String(200, "pong")
	})
	router.Run(":8080")
} </pre>
<div class="blog_h3"><span class="graybg">彩色输出</span></div>
<pre class="crayon-plain-tag">// 禁用控制台彩色输出
gin.DisableConsoleColor()
// 强制启用
gin.ForceConsoleColor()</pre>
<div class="blog_h3"><span class="graybg">输出到文件</span></div>
<pre class="crayon-plain-tag">f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)</pre>
<div class="blog_h2"><span class="graybg">数据绑定</span></div>
<div class="blog_h3"><span class="graybg">模型绑定和校验</span></div>
<p>要将请求体绑定到一个Go类型，可以使用模型绑定，目前<span style="background-color: #c0c0c0;">支持请求体格式：JSON、XML、YAML以及标准的表单数据</span>（a=b&amp;c=d）。你需要为所有需要绑定的字段设置Tag，例如<pre class="crayon-plain-tag">json:"fieldname"</pre>。</p>
<p>在校验方面，Gin使用go-playground/validator.v8，此项目提供了一系列<a href="http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags">校验用Tag</a>。</p>
<p>用于数据绑定的方法分为两类：</p>
<ol>
<li>必须绑定，包括<pre class="crayon-plain-tag">Bind</pre>、<pre class="crayon-plain-tag">BindJSON</pre>、<pre class="crayon-plain-tag">BindXML</pre>、<pre class="crayon-plain-tag">BindQuery</pre>、<pre class="crayon-plain-tag">BindYAML</pre>。这些方法在内部会调用<pre class="crayon-plain-tag">MustBindWith</pre>方法，如果绑定失败Gin会用 <pre class="crayon-plain-tag">c.AbortWithError(400, err).SetType(ErrorTypeBind)</pre>使请求失败，客户端将收到400 text/plain;charset=utf-8响应</li>
<li>应当绑定，包括ShouldBind、ShouldBindJSON、ShouldBindXML、ShouldBindQuery、ShouldBindYAML。这些方法在内部使用ShouldBindWith，如果绑定失败，你有机会进行处理</li>
</ol>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">type Login struct {
	//                                                   必须，如果binding:"-"则非必须
	User     string `form:"user" json:"user" xml:"user"  binding:"required"`
	Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

func main() {
	router := gin.Default()

	router.POST("/loginJSON", func(c *gin.Context) {
		var json Login
		//          绑定请求体到结构
		if err := c.ShouldBindJSON(&amp;json); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		
		if json.User != "manu" || json.Password != "123" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		} 
		
		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})

	// Example for binding XML (
	//	&lt;?xml version="1.0" encoding="UTF-8"?&gt;
	//	&lt;root&gt;
	//		&lt;user&gt;user&lt;/user&gt;
	//		&lt;password&gt;123&lt;/password&gt;
	//	&lt;/root&gt;)
	router.POST("/loginXML", func(c *gin.Context) {
		var xml Login
		//          类似，绑定XML格式的请求体
		if err := c.ShouldBindXML(&amp;xml); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		
		if xml.User != "manu" || xml.Password != "123" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		} 
		
		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})

	router.POST("/loginForm", func(c *gin.Context) {
		var form Login
		// 根据content-type决定使用何种方式进行绑定
		if err := c.ShouldBind(&amp;form); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		
		if form.User != "manu" || form.Password != "123" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		} 
		
		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})

	// Listen and serve on 0.0.0.0:8080
	router.Run(":8080")
} </pre>
<div class="blog_h3"><span class="graybg">表单/查询串</span></div>
<pre class="crayon-plain-tag">type StructD struct {
    NestedAnonyStruct struct {
        // 表单字段到结构字段的映射
        FieldX string `form:"field_x"`
    }
    FieldD string `form:"field_d"`
}

func GetDataD(c *gin.Context) {
    var b StructD
    // 绑定表单字段到结构
    c.Bind(&amp;b)
    c.JSON(200, gin.H{
        "x": b.NestedAnonyStruct,
        "d": b.FieldD,
    })
}

func main() {
    r := gin.Default()
    r.GET("/getd", GetDataD)

    r.Run()
}</pre>
<p>更多的Tag：</p>
<pre class="crayon-plain-tag">type Person struct {
	// 绑定日期
	Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}</pre>
<div class="blog_h3"><span class="graybg">绑定Checkbox</span></div>
<p>HTML：</p>
<pre class="crayon-plain-tag">&lt;form action="/" method="POST"&gt;
    &lt;p&gt;Check some colors&lt;/p&gt;
    &lt;label for="red"&gt;Red&lt;/label&gt;
    &lt;input type="checkbox" name="colors[]" value="red" id="red"&gt;
    &lt;label for="green"&gt;Green&lt;/label&gt;
    &lt;input type="checkbox" name="colors[]" value="green" id="green"&gt;
    &lt;label for="blue"&gt;Blue&lt;/label&gt;
    &lt;input type="checkbox" name="colors[]" value="blue" id="blue"&gt;
    &lt;input type="submit"&gt;
&lt;/form&gt;</pre>
<p>绑定到的结构：</p>
<pre class="crayon-plain-tag">type myForm struct {
    // 数组
    Colors []string `form:"colors[]"`
}</pre>
<p>处理器函数：</p>
<pre class="crayon-plain-tag">func formHandler(c *gin.Context) {
    var fakeForm myForm
    c.ShouldBind(&amp;fakeForm)
    c.JSON(200, gin.H{"color": fakeForm.Colors})
}</pre>
<div class="blog_h3"><span class="graybg">绑定URI路径变量</span></div>
<pre class="crayon-plain-tag">package main

import "github.com/gin-gonic/gin"

type Person struct {
	// 字段和URI路径变量映射
	ID string `uri:"id" binding:"required,uuid"`
	Name string `uri:"name" binding:"required"`
}

func main() {
	route := gin.Default()
	//        指定路径变量
	route.GET("/:name/:id", func(c *gin.Context) {
		var person Person
					// 绑定
		if err := c.ShouldBindUri(&amp;person); err != nil {
			c.JSON(400, gin.H{"msg": err})
			return
		}
		c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
	})
	route.Run(":8088")
}</pre>
<div class="blog_h3"><span class="graybg">参数绑定为Map</span></div>
<pre class="crayon-plain-tag">// 请求：
//            后缀不同的URL参数
// POST /post?ids[a]=1234&amp;ids[b]=hello HTTP/1.1
// Content-Type: application/x-www-form-urlencoded
// 特殊格式的表单参数
// names[first]=thinkerou&amp;names[second]=tianou

func main() {
	router := gin.Default()

	router.POST("/post", func(c *gin.Context) {
		// 绑定URL参数
		ids := c.QueryMap("ids")
		// 绑定表单参数
		names := c.PostFormMap("names")

		fmt.Printf("ids: %v; names: %v", ids, names)
	})
	router.Run(":8080")
}</pre>
<div class="blog_h2"><span class="graybg">数据校验</span></div>
<div class="blog_h3"><span class="graybg">内置校验器</span></div>
<pre class="crayon-plain-tag">package main

import (
    "gopkg.in/go-playground/validator.v9"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
)

type Booking struct {
    //                                  必须字段           隐含的格式校验
    CheckIn  time.Time `form:"check_in" binding:"required" time_format:"2006-01-02"`
                                                           // 值必须大于CheckIn字段
    CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}

func main() {
    route := gin.Default()

    route.GET("/bookable", getBookable)
    route.Run(":8085")
}

func getBookable(c *gin.Context) {
    var b Booking
    if err := c.ShouldBindWith(&amp;b, binding.Query); err == nil {
        c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
    } else {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    }
}</pre>
<div class="blog_h3"><span class="graybg">自定义校验器</span></div>
<pre class="crayon-plain-tag">package main

import (
    "gopkg.in/go-playground/validator.v9"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
)

type Booking struct {
    //                                                    使用定制校验器
    CheckIn  time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
    CheckOut time.Time `form:"check_out" binding:"required,bookabledate,gtfield=CheckIn" time_format:"2006-01-02"`
}

// 定制的校验器
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
    date, ok := fl.Field().Interface().(time.Time)
    if ok {
        today := time.Now()
        if today.After(date) {
            return false
        }
    }
    return true
}

func main() {
    route := gin.Default()
    // 注册校验器
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
                             // 该校验的Tag
        v.RegisterValidation("bookabledate", bookableDate)
    }

    route.GET("/bookable", getBookable)
    route.Run(":8085")
}</pre>
<div class="blog_h2"><span class="graybg">路由组</span></div>
<pre class="crayon-plain-tag">func main() {
	router := gin.Default()

	// 第一组路由
	v1 := router.Group("/v1")
	{
		//       /v1/login
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}

	// 第二组路由
	v2 := router.Group("/v2")
	{
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}

	router.Run(":8080")
}</pre>
<div class="blog_h2"><span class="graybg">服务器端渲染</span></div>
<div class="blog_h3"><span class="graybg">加载模板</span></div>
<pre class="crayon-plain-tag">router := gin.Default()
// 加载目录中所有模板
router.LoadHTMLGlob("templates/*")

// 加载多个文件
router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")

router.GET("/index", func(c *gin.Context) {
	// 渲染模板并输出
	c.HTML(http.StatusOK, "index.tmpl", gin.H{
		"title": "Main website",
	})
})


// 区分不同目录下的同名模板

router.LoadHTMLGlob("templates/**/*")

router.GET("/posts/index", func(c *gin.Context) {

    c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
        "title": "Posts",
    })
})
router.GET("/users/index", func(c *gin.Context) {
    c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
        "title": "Users",
    })
})</pre>
<div class="blog_h3"><span class="graybg">模板语法</span></div>
<p>使用Go Template，示例：</p>
<pre class="crayon-plain-tag">&lt;html&gt;
	&lt;h1&gt;
		{{ .title }}
	&lt;/h1&gt;
&lt;/html&gt;</pre>
<div class="blog_h2"><span class="graybg">HTTP2服务器推送</span></div>
<pre class="crayon-plain-tag">package main

import (
	"html/template"
	"log"

	"github.com/gin-gonic/gin"
)

var html = template.Must(template.New("https").Parse(`
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;Https Test&lt;/title&gt;
  &lt;script src="/assets/app.js"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1 style="color:red;"&gt;Welcome, Ginner!&lt;/h1&gt;
&lt;/body&gt;
&lt;/html&gt;
`))

func main() {
	r := gin.Default()
	// 静态目录
	r.Static("/assets", "./assets")
	r.SetHTMLTemplate(html)

	r.GET("/", func(c *gin.Context) {
		if pusher := c.Writer.Pusher(); pusher != nil {
			// 进行服务器推送
			if err := pusher.Push("/assets/app.js", nil); err != nil {
				log.Printf("Failed to push: %v", err)
			}
		}
		c.HTML(200, "https", gin.H{
			"status": "success",
		})
	})
}</pre>
<div class="blog_h2"><span class="graybg">JSONP</span></div>
<pre class="crayon-plain-tag">func main() {
	r := gin.Default()

	r.GET("/JSONP?callback=x", func(c *gin.Context) {
		data := map[string]interface{}{
			"foo": "bar",
		}
		
		//callback is x
		// Will output  :   x({\"foo\":\"bar\"})
		c.JSONP(http.StatusOK, data)
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/gin-study-note">Gin学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/gin-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>SOFAStack学习笔记</title>
		<link>https://blog.gmem.cc/sofastack-study-note</link>
		<comments>https://blog.gmem.cc/sofastack-study-note#comments</comments>
		<pubDate>Sun, 05 May 2019 08:10:37 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[SOFA]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=26795</guid>
		<description><![CDATA[<p>简介 SOFAStack（Scalable Open Financial Architecture Stack，可扩展开放金融架构栈）是蚂蚁金服开源的技术栈，国内多家金融和互联网公司在生产环境使用了此技术栈。 SOFABoot 基于Spring Boot，额外提供了以下特性： 健康检查（Readiness探针）：在Spring Boot Liveness探针的基础上增加Readiness探针，仅当此探针成功后实例才能接受流量 类（加载器）隔离，基于SOFAArk，实现业务代码的类、SOFA中间件相关的类的隔离，避免了类冲突 日志空间隔离：将SOFA中间件日志和业务代码产生的日志分开 和其他SOFA中间件便捷的集成：提供各种SOFA中间件的Starter 模块化：可以为同一JVM中运行的不同SOFABoot模块提供独立的Spring ApplicationContext，可以规避BeanId的冲突 SOFABoot需要JDK 7+和Maven 3.2.5来完成构建。 起步 创建项目 <a class="read-more" href="https://blog.gmem.cc/sofastack-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/sofastack-study-note">SOFAStack学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>SOFAStack（Scalable Open Financial Architecture Stack，可扩展开放金融架构栈）是蚂蚁金服开源的技术栈，国内多家金融和互联网公司在生产环境使用了此技术栈。</p>
<div class="blog_h1"><span class="graybg">SOFABoot</span></div>
<p>基于Spring Boot，额外提供了以下特性：</p>
<ol>
<li>健康检查（<span style="background-color: #c0c0c0;">Readiness探针</span>）：在Spring Boot Liveness探针的基础上增加Readiness探针，仅当此探针成功后实例才能接受流量</li>
<li><span style="background-color: #c0c0c0;">类（加载器）隔离</span>，基于SOFAArk，实现业务代码的类、SOFA中间件相关的类的隔离，避免了类冲突</li>
<li><span style="background-color: #c0c0c0;">日志空间隔离</span>：将SOFA中间件日志和业务代码产生的日志分开</li>
<li>和其他SOFA中间件便捷的集成：提供各种<span style="background-color: #c0c0c0;">SOFA中间件的Starter</span></li>
<li>模块化：可以为同一JVM中运行的不同SOFABoot模块提供<span style="background-color: #c0c0c0;">独立的Spring ApplicationContext</span>，可以规避BeanId的冲突</li>
</ol>
<p>SOFABoot需要JDK 7+和Maven 3.2.5来完成构建。</p>
<div class="blog_h2"><span class="graybg">起步</span></div>
<div class="blog_h3"><span class="graybg">创建项目</span></div>
<p>通过IntelliJ IDEA的Spring Initializer来创建项目骨架，依赖选择Web。然后修改POM，将parent改为：</p>
<pre class="crayon-plain-tag">&lt;parent&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;sofaboot-dependencies&lt;/artifactId&gt;
    &lt;version&gt;${sofa.boot.version}&lt;/version&gt;
&lt;/parent&gt;</pre>
<p>并添加依赖： </p>
<pre class="crayon-plain-tag">&lt;!-- 提供健康检查能力 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;healthcheck-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
     &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
     &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
<p>SOFABoot要求提供必要的参数：</p>
<pre class="crayon-plain-tag">spring.application.name=Learing SOFABoot
logging.path=./logs</pre>
<div class="blog_h3"><span class="graybg">执行探针</span></div>
<p>启动上述项目后，运行命令<pre class="crayon-plain-tag">curl http://localhost:8080/actuator/health</pre>，可以看到服务的健康状况，正常情况下输出<pre class="crayon-plain-tag">{"status":"UP"}</pre></p>
<p>运行命令<pre class="crayon-plain-tag">curl -s http://localhost:8080/actuator/readiness | jq .</pre>可以获得Readiness探针执行结果的细节：</p>
<pre class="crayon-plain-tag">{
  "details": {
    "diskSpace": {
      "details": {
        "threshold": 10485760,
        "free": 166560387072,
        "total": 358796750848
      },
      "status": "UP"
    },
    "SOFABootReadinessHealthCheckInfo": {
      "status": "UP"
    }
  },
  "status": "UP"
}</pre>
<div class="blog_h3"><span class="graybg">查看日志</span></div>
<p>检查logs目录，可以看到日志根据来源的不同，分割到logs、infra等目录中。 </p>
<div class="blog_h3"><span class="graybg">单元测试</span></div>
<p>你可以使用SpringRunner进行单元测试，但是如果在项目中使用了SOFABoot的类隔离特性，则必须使用SofaBootRunner、SofaJUnit4Runner进行单元测试，并引入依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;test-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
<p>单元测试用例的例子：</p>
<pre class="crayon-plain-tag">@SpringBootTest
@RunWith(SpringRunner.class)
public class SofaBootWithModulesTest {
    // 引用SOFABoot模块发布的服务
    @SofaReference
    private SampleJvmService sampleJvmService;

    @Test
    public void test() {
        Assert.assertEquals("Hello, jvm service xml implementation.", sampleJvmService.message());
    }
} </pre>
<div class="blog_h3"><span class="graybg">模块化开发</span></div>
<p>从2.4版本开始SOFABoot引入了基于Spring上下文隔离的模块化能力。SOFABoot定义了三个模块化隔离级别：</p>
<ol>
<li>代码组织上的模块化：开发阶段分多个工程开发，例如使用Dubbo的团队，通常都会将API拆分为独立的Maven模块。这种隔离对运行时没有任何影响</li>
<li>基于Spring上下文隔离的模块化“类似于1，代码和配置分散在不同工程中。但是在运行时会启动多个Spring上下文（它们有共同的父上下文），DI仅仅在子上下文内发生。可以规避BeanId冲突问题</li>
<li>基于类加载器隔离的模块化：每个模块都使用独立的类加载器，可以规避模块依赖了冲突的类版本的问题</li>
</ol>
<p>SOFABoot模块化开发属于第二种模块化形式 —— 基于 Spring 上下文隔离的模块化，每个模块使用独立的Spring上下文。</p>
<p>每个SOFABoot模块包含<span style="background-color: #c0c0c0;">Java代码、Spring配置文集、SOFA模块标识</span>等信息，打包形式为JAR。不同模块之间不能通过DI来引用，需要转而使用SOFA服务。SOFABoot支持两种形式的服务（发布/引用）：</p>
<ol>
<li><span style="background-color: #c0c0c0;">JVM服务</span>发布和引用：解决同一SOFABoot 应用内各 SOFABoot 模块之间的调用问题</li>
<li><span style="background-color: #c0c0c0;">RPC服务</span>发布和引用：解决多个 SOFABoot 应用之间的远程调用问题</li>
</ol>
<p>由于相互之间没有Bean依赖，SOFABoot模块可以并行的启动，这可以提升应用启动速度。</p>
<p>这种模块化开发，和微服务的理念是违背的，各模块仍然需要共享单个JVM的资源。</p>
<p>为了体验SOFABoot的模块化开发，我们直接使用其源码附带的示例应用：</p>
<pre class="crayon-plain-tag">git clone https://github.com/alipay/sofa-boot.git
tree sofa-boot/sofaboot-samples/sofaboot-sample-with-isle -L 1
# .
# ├── service-consumer    服务的消费者
# ├── service-facade      服务的API
# ├── service-provider    服务的提供者
# ├── sofa-boot-run       启动包含模块的SOFABoot应用</pre>
<p>service-facade中定义的接口如下：</p>
<pre class="crayon-plain-tag">public interface SampleJvmService {
    String message();
}</pre>
<p>service-provider将上面的接口发布为JVM服务。发布方式有三种：</p>
<ol>
<li>注解方式：<br />
<pre class="crayon-plain-tag">//           唯一性的ID，不设置默认为空串
@SofaService(uniqueId = "annotationImpl")
public class SampleJvmServiceAnnotationImpl implements SampleJvmService {
    @Override
    public String message() {
        String message = "Hello, jvm service annotation implementation.";
        System.out.println(message);
        return message;
    }
}</pre>
</li>
<li>
<p>XML方式： </p>
<pre class="crayon-plain-tag">public class SampleJvmServiceImpl implements SampleJvmService {
    private String message;

    @Override
    public String message() {
        System.out.println(message);
        return message;
    }
}</pre>
<p>需要配合XML配置： </p>
<pre class="crayon-plain-tag">&lt;bean id="sampleJvmService" class="com.alipay.sofa.isle.sample.SampleJvmServiceImpl"&gt;
    &lt;property name="message" value="Hello, jvm service xml implementation."/&gt;
&lt;/bean&gt;

&lt;sofa:service ref="sampleJvmService" interface="com.alipay.sofa.isle.sample.SampleJvmService"&gt;
    &lt;sofa:binding.jvm/&gt;
&lt;/sofa:service&gt;</pre>
</li>
<li>编程式：<br />
<pre class="crayon-plain-tag">@Component
public class PublishServiceWithClient implements ClientFactoryAware {
    private ClientFactory clientFactory;
    
    @PostConstruct
    public void init() {
        ServiceClient serviceClient = clientFactory.getClient(ServiceClient.class);
        ServiceParam serviceParam = new ServiceParam();
        serviceParam.setInstance(new SampleJvmServiceImpl( "Hello, jvm service service client implementation."));
        serviceParam.setInterfaceType(SampleJvmService.class);
        serviceParam.setUniqueId("serviceClientImpl");
        // 发布服务
        serviceClient.service(serviceParam);
    }

    @Override
    public void setClientFactory(ClientFactory clientFactory) {
        this.clientFactory = clientFactory;
    }
} </pre>
</li>
</ol>
<p>你需要在sofa-module.properties中声明模块名称、模块依赖：
<pre class="crayon-plain-tag"># service-provider
Module-Name=com.alipay.sofa.service-provider

# service-consumer
Module-Name=com.alipay.sofa.service-consumer
Require-Module=com.alipay.sofa.service-provider</pre>
<p>service-consumer负责消费service-provider发布的服务：</p>
<pre class="crayon-plain-tag">public class JvmServiceConsumer implements ClientFactoryAware {
    private ClientFactory    clientFactory;

    // 引用基于XML配置的JVM服务
    @Autowired
    private SampleJvmService sampleJvmService;
    // 引用基于注解配置的JVM服务，使用uniqueId
    @SofaReference(uniqueId = "annotationImpl")
    private SampleJvmService sampleJvmServiceByFieldAnnotation;

    public void init() {
        sampleJvmService.message();
        sampleJvmServiceByFieldAnnotation.message();

        // 编程式客户端
        ReferenceClient referenceClient = clientFactory.getClient(ReferenceClient.class);
        ReferenceParam referenceParam = new ReferenceParam&lt;&gt;();
        referenceParam.setInterfaceType(SampleJvmService.class);
        referenceParam.setUniqueId("serviceClientImpl");
        SampleJvmService sampleJvmServiceClientImpl = referenceClient.reference(referenceParam);
        sampleJvmServiceClientImpl.message();
    }

    @Override
    public void setClientFactory(ClientFactory clientFactory) {
        this.clientFactory = clientFactory;
    }
}</pre>
<div class="blog_h3"><span class="graybg">使用SOFA栈</span></div>
<p>如果需要在SOFABoot项目中使用SOFA中间件，需要依赖相应的Starter： </p>
<pre class="crayon-plain-tag">&lt;!-- SOFARPC --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;rpc-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;!-- SOFATracer --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;tracer-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;!-- SOFALookout --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;lookout-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
<p> 如果需要在SOFABoot项目中使用扩展组件，需要依赖相应的Starter： </p>
<pre class="crayon-plain-tag">&lt;!-- 健康检查扩展 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;healthcheck-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;


&lt;!-- 模块化隔离扩展 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;isle-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;


&lt;!-- 类隔离扩展 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;sofa-ark-springboot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;


&lt;!-- 测试扩展 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;test-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
<p>如果要使用SOFAArk提供的类加载器隔离功能，则需要依赖相应的ARK插件，并替换到对应的Starter：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">ARK插件</td>
<td style="text-align: center;">ArtifactId</td>
</tr>
</thead>
<tbody>
<tr>
<td>SOFARPC</td>
<td>rpc-sofa-boot-plugin</td>
</tr>
<tr>
<td>SOFATracer</td>
<td>tracer-sofa-boot-plugin</td>
</tr>
</tbody>
</table>
<p>ARK插件能够让业务应用的类、SOFA中间件的类（以及它依赖的类）使用不同的类加载器，从而避免类冲突问题。</p>
<div class="blog_h2"><span class="graybg">健康检查</span></div>
<p>Spring Boot <span style="background-color: #c0c0c0;">Actuator提供的HealthIndicator接口可以提供Liveness健康检查</span>，SOFABoot在此基础上增加了Readiness检查功能。使用SOFA中间件时，最好配合SOFABoot的健康检查，以实现优雅上线，避免未准备好的实例过早加入服务池。</p>
<p>健康检查扩展提供了多个扩展点，允许用户定制其行为。</p>
<p>健康检查相关配置属性：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">配置属性</td>
<td style="text-align: center;">说明</td>
<td style="width: 80px; text-align: center;">默认值</td>
</tr>
</thead>
<tbody>
<tr>
<td>com.alipay.sofa.healthcheck.skip.all</td>
<td>是否跳过整个 Readiness Check 阶段</td>
<td>false</td>
</tr>
<tr>
<td>com.alipay.sofa.healthcheck.skip.component</td>
<td>是否跳过 SOFA 中间件的 Readiness Check</td>
<td>false</td>
</tr>
<tr>
<td>com.alipay.sofa.healthcheck.skip.indicator</td>
<td>是否跳过 HealthIndicator 的 Readiness Check</td>
<td>false</td>
</tr>
<tr>
<td>com.alipay.sofa.healthcheck.component.check.retry.count</td>
<td>组件健康检查重试次数</td>
<td>20</td>
</tr>
<tr>
<td>com.alipay.sofa.healthcheck.component.check.retry.interval</td>
<td>组件健康检查重试间隔时间</td>
<td>1000ms</td>
</tr>
<tr>
<td>com.alipay.sofa.healthcheck.module.check.retry.count</td>
<td>sofaboot 模块健康检查重试次数</td>
<td>0</td>
</tr>
<tr>
<td>com.alipay.sofa.healthcheck.module.check.retry.interval</td>
<td>sofaboot 模块健康检查重试间隔时间</td>
<td>1000ms</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">异步Bean初始化</span></div>
<p>SOFABoot支持异步的执行Bean的初始化方法，要使用该特性，引入依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;runtime-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
<p>并且为目标Bean提供配置属性async-init：</p>
<pre class="crayon-plain-tag">&lt;bean id="testBean" class="com.alipay.sofa.runtime.beans.TimeWasteBean" init-method="init" async-init="true"/&gt;</pre>
<p>SOFABoot在独立的线程池中进行Bean的异步初始化，相关配置属性：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">配置属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>com.alipay.sofa.boot.asyncInitBeanCoreSize</td>
<td>线程池默认线程数</td>
</tr>
<tr>
<td>com.alipay.sofa.boot.asyncInitBeanMaxSize</td>
<td>线程池最大线程数</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">模块隔离</span></div>
<div class="blog_h3"><span class="graybg">JAR包格式</span></div>
<p>SOFABoot模块的JAR包遵循如下约定：</p>
<ol>
<li>包含文件sofa-module.properties，定义模块名称、模块之间的依赖关系等元数据</li>
<li>META-INF/spring目录下的任意Spring配置文件都会作为本模块的配置而加载</li>
</ol>
<div class="blog_h3"><span class="graybg">模块元数据</span></div>
<p>模块元数据声明在sofa-module.properties文件中，包含以下配置属性：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">配置属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Module-Name</td>
<td>SOFABoot模块名称，唯一标识，Java包路径形式</td>
</tr>
<tr>
<td>Spring-Parent</td>
<td>指定一个模块的名称，将其Spring上下文设置为当前模块的父上下文，这样就可以解除对目标模块的隔离</td>
</tr>
<tr>
<td>Require-Module</td>
<td>逗号分隔的，依赖的其它模块列表</td>
</tr>
<tr>
<td>Module-Profile</td>
<td>
<p>所属的Profile，通过Spring配置属性<pre class="crayon-plain-tag">com.alipay.sofa.boot.active-profile</pre>，可以指定哪些SOFABoot Profile启用（逗号分隔多个值）</p>
<p>只有其Profile启用的模块才激活（也就是启动）</p>
<p>此外，在Spring配置文件中，可以嵌套beans元素，子元素可以指定profile属性，来指定仅当特定Profile启用时才激活的Bean：</p>
<pre class="crayon-plain-tag">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context.xsd"
       default-autowire="byName"&gt;       

    &lt;beans profile="dev"&gt;
        &lt;bean id="devBeanId" class="com.alipay.cloudenginetest.sofaprofilesenv.DemoBean"&gt;
        &lt;/bean&gt;
    &lt;/beans&gt;

    &lt;beans profile="test"&gt;
        &lt;bean id="testBeanId" class="com.alipay.cloudenginetest.sofaprofilesenv.DemoBean"&gt;
        &lt;/bean&gt;
    &lt;/beans&gt;
&lt;/beans&gt;</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">集成SOFARPC</span></div>
<p>参考<a href="#sofagrpc-with-sofoboot">SOFARPC - 起步 - 整合SOFABoot</a>一节。 
<div class="blog_h1"><span class="graybg">SOFAArk</span></div>
<p>SOFAArk 是一款基于 Java 实现的轻量级类隔离容器，提供类隔离和应用（模块）合并部署能力。</p>
<p>在大型软件开发过程中，通常会推荐<span style="background-color: #c0c0c0;">底层功能插件化，业务功能模块化</span>的开发模式，以期达到低耦合、高内聚、功能复用的优点。SOFAArk 提供了一套插件化、模块化的开发规范，它的能力包括：</p>
<ol>
<li>定义类加载模型，运行时底层插件、业务应用（模块）的类相互隔离，每个插件和应用（模块）由不同的 ClassLoader 加载，可以有效避免相互之间的包冲突</li>
<li>定义了插件开发规范，可以基于Maven将多个第三方JAR打包为<span style="background-color: #c0c0c0;">Ark Plugin（插件）</span></li>
<li>定义模块开发规范，可以基于Maven将应用打包为<span style="background-color: #c0c0c0;">Ark Biz（模块）</span></li>
<li>提供针对Plugin、Biz的标准API，提供事件、扩展点支持</li>
<li>多Biz合并部署，打包为扁平的可执行JAR</li>
<li>支持在运行时通过API或配置中心来动态安装/写在Biz</li>
</ol>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>Ark 包是<span style="background-color: #c0c0c0;">满足特定目录格式要求的可执行扁平</span>（Flat，也就是打包了所有依赖）Jar，使用Maven 插件 <pre class="crayon-plain-tag">sofa-ark-maven-plugin</pre>可以将<span style="background-color: #c0c0c0;">单个或多个应用打包</span>成标准格式的 Ark 包，Ark包包含三类构件：</p>
<ol>
<li>Ark Container：负责 Ark 包启动、运行时的管理，<span style="background-color: #c0c0c0;">Ark Plugin 和 Ark Biz 运行在 SOFAArk 容器之上</span>。Ark Container具备<span style="background-color: #c0c0c0;">管理插件和应用的能力</span>，<span style="background-color: #c0c0c0;">容器启动成功后，会自动解析类路径包含的 Ark Plugin 和 Ark Biz 依赖，完成隔离加载并按优先级依次启动它们</span></li>
<li>Ark Plugin，使用Maven插件<pre class="crayon-plain-tag">sofa-ark-maven-plugin</pre>可以将单个或多个普通Jar包打包为Ark Plugin。Ark Plugin有一个配置文件，其中包含：插件类导入导出配置、资源导入导出配置、插件启动优先级等信息。SOFAArk使用独立的PluginClassLoader来加载插件</li>
<li>Ark Biz，使用Maven插件<pre class="crayon-plain-tag">sofa-ark-maven-plugin</pre>可以将业务应用打包为Biz包。<span style="background-color: #c0c0c0;">每个Ark包可以包含多个Biz，他们按优先级依次启动，通过JVM服务交互</span></li>
</ol>
<p>执行Ark包时，Ark Container优先启动，然后启动Plugin，最后启动Biz。它们的逻辑关系图如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/05/sofa-ark-arch.png"><img class="aligncenter  wp-image-26907" src="https://blog.gmem.cc/wp-content/uploads/2019/05/sofa-ark-arch.png" alt="sofa-ark-arch" width="876" height="793" /></a></p>
<div class="blog_h2"><span class="graybg">类冲突问题</span></div>
<p>在解决类冲突（两个运行在单个JVM中的模块依赖某个类的两个不兼容版本）方面，SOFAArk比OSGI简单的多。SOFAArk 提出了一种特殊的包结构Ark Plugin，在存在<span style="background-color: #c0c0c0;">包冲突时，用户可以使用 Maven 插件将若干冲突包打包成 Plugin</span>，运行时由<span style="background-color: #c0c0c0;">独立的 PluginClassLoader 加载</span>，从而解决包冲突。</p>
<div class="blog_h2"><span class="graybg">合并部署</span></div>
<p>将复杂项目拆分到多个工程的主要动机包括避免VCS提交冲突、规避技术栈导致的依赖冲突。SOFA支持模块化，可以<span style="background-color: #c0c0c0;">将多个业务模块打包为Biz，在运行时合并部署到单个JVM，各模块基于同一的API进行交互</span>。</p>
<div class="blog_h3"><span class="graybg">静态合并</span></div>
<p>这种模式下，Biz之间的依赖可以通过Maven管理，当应用打为扁平化JAR时其依赖的Biz可以合并进来。<span style="background-color: #c0c0c0;">每个Biz使用独立的BizClassLoader加载</span>，<span style="background-color: #c0c0c0;">Biz之间通过JVM服务（SofaService/SofaRefernece）进行交互</span>。</p>
<div class="blog_h3"><span class="graybg">动态合并</span></div>
<p>这种模式下，Biz可以在运行时，通过API或者配置中心（ZooKeeper）来动态部署/卸载。</p>
<p>这里需要提到主应用（Master Biz）的概念，其实不管静态/动态合并，此概念都是存在的。如果Ark包打了单个Biz则它就是主应用，如果打了多个Biz包则需要配置指定主应用。主应用不得卸载。</p>
<p>通常情况下，各模块的实现放在动态Biz中，供主应用调用。主应用通过两种方式部署/卸载动态Biz：</p>
<ol>
<li>调用SOFAArk的API</li>
<li>使用SOFAArk的Config插件，对接ZooKeeper配置中心。此插件会解析配置并控制动态Biz的部署/卸载</li>
</ol>
<div class="blog_h2"><span class="graybg">插件开发</span></div>
<p>Ark Plugin可以导入、导出类或资源：</p>
<ol>
<li>
<p>导入类：插件启动时，<span style="background-color: #c0c0c0;">优先委托给导出该类的插件负责加载</span>，如果加载不到，才会尝试从本插件内部加载</p>
</li>
<li>
<p>导出类：其<span style="background-color: #c0c0c0;">他插件如果导入了该类，优先从本插件加载</span></p>
</li>
<li>
<p>导入资源：插件在查找资源时，优先委托给导出该资源的插件负责加载，如果加载不到，才会尝试从本插件内部加载</p>
</li>
<li>
<p>导出资源：其他插件如果导入了该资源，优先从本插件加载</p>
</li>
</ol>
<p>SOFAArk的代码库提供了一个样例项目。其结构如下：</p>
<pre class="crayon-plain-tag">├── sample-ark-plugin
│   ├── common    # 此模块包含了插件导出类
│   ├── plugin    # 包含插件服务的实现、PluginActivator接口实现
│   ├── pom.xml</pre>
<div class="blog_h3"><span class="graybg">插件配置</span></div>
<p>plugin项目的POM中包含如下配置：</p>
<pre class="crayon-plain-tag">&lt;build&gt;
    &lt;plugins&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
            &lt;artifactId&gt;sofa-ark-plugin-maven-plugin&lt;/artifactId&gt;
            &lt;version&gt;${project.version}&lt;/version&gt;
            &lt;executions&gt;
                &lt;execution&gt;
                    &lt;id&gt;default-cli&lt;/id&gt;
                    &lt;goals&gt;
                        &lt;goal&gt;ark-plugin&lt;/goal&gt;
                    &lt;/goals&gt;

                    &lt;configuration&gt;

                        &lt;!-- Ark 容器启动插件的入口类，最多只能配置一个。一般在此执行初始化操作，比如发布插件服务 --&gt;
                        &lt;activator&gt;com.alipay.sofa.ark.sample.activator.SamplePluginActivator&lt;/activator&gt;

                        &lt;!-- 导出的包、类、资源列表 --&gt;
                        &lt;exported&gt;
                            &lt;packages&gt;
                                &lt;package&gt;com.alipay.sofa.ark.sample.common&lt;/package&gt;
                            &lt;/packages&gt;
                            &lt;classes&gt;
                                &lt;class&gt;com.alipay.sofa.ark.sample.facade.SamplePluginService&lt;/class&gt;
                            &lt;/classes&gt;
                        &lt;/exported&gt;

                        &lt;!-- 打包后的插件的存放位置，默认${project.build.directory} --&gt;
                        &lt;outputDirectory&gt;../target&lt;/outputDirectory&gt;

                    &lt;/configuration&gt;
                &lt;/execution&gt;

            &lt;/executions&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;</pre>
<p>此配置声明了当前插件的一切必要信息。</p>
<div class="blog_h3"><span class="graybg">发布服务</span></div>
<p>SamplePluginActivator负责在插件启动时，<span style="background-color: #c0c0c0;">发布JVM服务</span>：</p>
<pre class="crayon-plain-tag">package com.alipay.sofa.ark.sample.activator;

import com.alipay.sofa.ark.exception.ArkRuntimeException;
import com.alipay.sofa.ark.sample.facade.SamplePluginService;
import com.alipay.sofa.ark.sample.impl.SamplePluginServiceImpl;
import com.alipay.sofa.ark.spi.model.PluginContext;
import com.alipay.sofa.ark.spi.service.PluginActivator;

public class SamplePluginActivator implements PluginActivator {

    // 插件启动时的回调
    public void start(PluginContext context) throws ArkRuntimeException {
        // 通过插件上下文发布服务
        context.publishService(SamplePluginService.class, new SamplePluginServiceImpl());
    }

    // 插件停止时的回调
    public void stop(PluginContext context) throws ArkRuntimeException {
        System.out.println("stopping in ark plugin activator");
    }

}</pre>
<div class="blog_h3"><span class="graybg">订阅服务</span></div>
<p>除了发布服务之外，你还可以<span style="background-color: #c0c0c0;">引用其他插件或Ark容器发布的服务</span>：</p>
<pre class="crayon-plain-tag">public class SamplePluginServiceImpl implements SamplePluginService {

    // 引用Ark容器发布的，事件管理服务
    @ArkInject
    private EventAdminService eventAdminService;

    public String service() {
        return "I'm a sample plugin service published by ark-plugin";
    }

    public void sendEvent(ArkEvent arkEvent) {
        eventAdminService.sendEvent(arkEvent);
    }
}</pre>
<div class="blog_h2"><span class="graybg">打Ark包</span></div>
<p>使用sofa-ark-maven-plugin也可以把普通的Spring Boot项目打为Ark包：</p>
<pre class="crayon-plain-tag">&lt;build&gt;
    &lt;plugins&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
            &lt;artifactId&gt;sofa-ark-maven-plugin&lt;/artifactId&gt;
            &lt;executions&gt;
                &lt;execution&gt;
                    &lt;id&gt;default-cli&lt;/id&gt;
                    
                    &lt;!-- 此目标生成可执行Ark包 --&gt;
                    &lt;goals&gt;
                        &lt;goal&gt;repackage&lt;/goal&gt;
                    &lt;/goals&gt;
                    
                    &lt;configuration&gt;
                        &lt;!-- 可执行Ark包的输出目录 --&gt;
                        &lt;outputDirectory&gt;./target&lt;/outputDirectory&gt;
                        &lt;!-- 可以指定Ark包的Maven坐标的classifier字段 --&gt;
                        &lt;arkClassifier&gt;executable-ark&lt;/arkClassifier&gt;
                    &lt;/configuration&gt;
                &lt;/execution&gt;
            &lt;/executions&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;</pre>
<div class="blog_h2"><span class="graybg">运行Ark包</span></div>
<div class="blog_h3"><span class="graybg">SpringBoot</span></div>
<p>添加以下依赖即可：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;sofa-ark-springboot-starter&lt;/artifactId&gt;
    &lt;version&gt;${sofa.ark.version}&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">独立Java工程 </span></div>
<p>需要添加依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;sofa-ark-support-starter&lt;/artifactId&gt;
    &lt;version&gt;${sofa.ark.version}&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>并且在入口点方法中启动Ark容器：</p>
<pre class="crayon-plain-tag">public class Application{
    public static void main(String[] args) { 
        SofaArkBootstrap.launch(args);
    }
}</pre>
<div class="blog_h3"><span class="graybg">单元测试</span></div>
<p>SOFAArk提供了org.junit.runner.Runner的实现类ArkJUnit4Runner，用于集成JUnit4单元测试框架：</p>
<pre class="crayon-plain-tag">@RunWith(ArkJUnit4Runner.class)
public class JUnitTest {

    @Test
    public void test() {
        Assert.assertTrue(true);
    }</pre>
<p>上面的用例会在Ark容器之上运行。 </p>
<div class="blog_h3"><span class="graybg">集成测试</span></div>
<p>SOFAArk提供了org.junit.runner.Runner的实现类ArkBootRunner，用于在Spring Boot下进行集成测试：</p>
<pre class="crayon-plain-tag">@RunWith(ArkBootRunner.class)
@SpringBootTest(classes = SpringbootDemoApplication.class)
public class IntegrationTest {

    // 可以注入依赖
    @Autowired
    private SampleService sampleService;

    @Test
    public void test() {
        sampleService.service();
    }

} </pre>
<div class="blog_h1"><span class="graybg">SOFARPC</span></div>
<p>这是一个Java的RPC框架，提供了负载均衡，流量转发，链路追踪，链路数据透传，故障剔除等特性，<span style="background-color: #c0c0c0;">兼容 bolt，RESTful，dubbo，H2C协议</span>。</p>
<p>SOFARPC的基本工作原理和Dubbo类似：</p>
<ol>
<li>如果当前应用需要发布RPC服务，那么SOFARPC会将服务注册到服务注册中心</li>
<li>如果当前应用需要调用RPC服务，那么SOFARPC会到注册中心订阅相关服务的元数据。注册中心会即时的推送元数据更新，例如服务的端点信息</li>
</ol>
<div class="blog_h2"><span class="graybg">独立运行</span></div>
<p>需要加入依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;sofa-rpc-all&lt;/artifactId&gt;
    &lt;version&gt;5.6.0-SNAPSHOT&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>需要发布的服务接口： </p>
<pre class="crayon-plain-tag">public interface HelloService {
    String sayHello(String string);
}</pre>
<p>接口实现HelloServiceImpl这里略去。</p>
<p>SOFARPC服务器：</p>
<pre class="crayon-plain-tag">public class QuickStartServer {

    public static void main(String[] args) {

        ServerConfig serverConfig = new ServerConfig()
            .setProtocol("bolt") // 设置一个协议，默认bolt
            .setPort(12200) // 设置一个端口，默认12200
            .setDaemon(false); // 非守护线程方式运行

        // 使用ZK作为注册表
        RegistryConfig registryConfig = new RegistryConfig()
            .setProtocol("zookeeper")
            .setAddress("127.0.0.1:2181");

        ProviderConfig providerConfig = new ProviderConfig()
            .setInterfaceId(HelloService.class.getName()) // 指定接口
            .setRef(new HelloServiceImpl()) // 指定实现
            .setServer(serverConfig)  // 指定服务端
            .setRegistry(registryConfig);  // 服务注册表

        providerConfig.export(); // 发布服务
    }
}</pre>
<p>SOFARPC客户端： </p>
<pre class="crayon-plain-tag">public class QuickStartClient {

    private final static Logger LOGGER = LoggerFactory.getLogger(QuickStartClient.class);

    public static void main(String[] args) {

        ConsumerConfig consumerConfig = new ConsumerConfig()
            .setInterfaceId(HelloService.class.getName()) // 指定接口
            .setProtocol("bolt") // 指定协议
            .setDirectUrl("bolt://127.0.0.1:12200") // 指定服务端的直连地址
            .setConnectTimeout(10 * 1000);

        HelloService helloService = consumerConfig.refer();

        helloService.sayHello("world");
    }
}</pre>
<div class="blog_h2"><span class="graybg"><a id="sofagrpc-with-sofoboot"></a>整合SOFABoot</span></div>
<p>SpringBoot应用的名字必须配置：</p>
<pre class="crayon-plain-tag">spring.application.name=test </pre>
<p>你需要为SOFABoot工程引入SOFARPC的starter：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
     &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
     &lt;artifactId&gt;rpc-sofa-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
<p>你可以以POJO的形式定义服务接口和它的实现。</p>
<div class="blog_h3"><span class="graybg">XML方式</span></div>
<p>发布服务时，可以使用下面的XML配置：</p>
<pre class="crayon-plain-tag">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:sofa="http://sofastack.io/schema/sofaboot"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://sofastack.io/schema/sofaboot   http://sofastack.io/schema/sofaboot.xsd"
       default-autowire="byName"&gt;
    
    &lt;!-- 服务的Bean定义 --&gt;
    &lt;bean id="personServiceImpl" class="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonServiceImpl"/&gt;

    &lt;!-- 发布SOFARPC服务 --&gt;
    &lt;sofa:service ref="personServiceImpl" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService"&gt;
        &lt;!-- 绑定的通信协议  --&gt;
        &lt;sofa:binding.bolt&gt;
            &lt;sofa:global-attrs timeout="3000" address-wait-time="2000"/&gt;  &lt;!-- 调用超时；地址等待时间 --&gt;
            &lt;sofa:route target-url="127.0.0.1:22000"/&gt;  &lt;!-- 直连到提供者，不走负载均衡 --&gt;
            &lt;sofa:method name="sayName" timeout="3000"/&gt; &lt;!-- 方法级别超时配置 --&gt;
        &lt;/sofa:binding.bolt&gt;
        &lt;sofa:binding.rest/&gt;
    &lt;/sofa:service&gt;

    &lt;!-- 订阅SOFARPC服务 --&gt;
    &lt;sofa:reference id="personReferenceBolt" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService"&gt;
        &lt;sofa:binding.bolt/&gt;
    &lt;/sofa:reference&gt;

    &lt;sofa:reference id="personReferenceRest" interface="com.alipay.sofa.boot.examples.demo.rpc.bean.PersonService"&gt;
        &lt;sofa:binding.rest/&gt;
    &lt;/sofa:reference&gt;

&lt;/beans&gt;</pre>
<div class="blog_h3"><span class="graybg">注解方式</span></div>
<pre class="crayon-plain-tag">// 发布服务
@SofaService(interfaceType = AnnotationService.class, 
               bindings = { 
                 @SofaServiceBinding(bindingType = "bolt"),
                 @SofaServiceBinding(bindingType = "bolt") 
               })
@Component
public class AnnotationServiceImpl implements AnnotationService {
    @Override
    public String sayAnnotation(String stirng) {
        return stirng;
    }
}

// 订阅服务
@Component
public class AnnotationClientImpl {

    @SofaReference(interfaceType = AnnotationService.class, 
                     binding = @SofaReferenceBinding(bindingType = "bolt"))
    private AnnotationService annotationService;

    public String sayClientAnnotation(String str) {

        String result = annotationService.sayAnnotation(str);

        return result;
    }
}</pre>
<div class="blog_h2"><span class="graybg">使用过滤器</span></div>
<p>SOFARPC支持过滤器。实现过滤器非常简单：</p>
<pre class="crayon-plain-tag">public class PersonServiceFilter extends Filter {
    @Override
    public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException {
        // 前置钩子
        System.out.println("PersonFilter before");
        try {
            // 执行RPC调用
            return invoker.invoke(request);
        } finally {
        // 后置钩子
            System.out.println("PersonFilter after");
        }
    }
}</pre>
<p>过滤器需要配置才能生效。</p>
<div class="blog_h2"><span class="graybg">Bolt协议</span></div>
<p>Bolt是一个高性能的TCP协议，其性能比HTTP好。</p>
<div class="blog_h3"><span class="graybg">调用类型</span></div>
<p>Bolt支持<span style="background-color: #c0c0c0;">同步、异步、回调、单向</span>等多种调用类型：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">调用类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Synchronous</td>
<td>默认的调用类型，调用发起后，当前线程会阻塞以等待结果</td>
</tr>
<tr>
<td>Asynchronous</td>
<td>
<p>调用发起后，当前线程立即处理后续逻辑，当调用结果到达后SOFAGRPC会缓存之，你可以异步的调用API以获得结果</p>
<p>XML配置：</p>
<pre class="crayon-plain-tag">&lt;sofa:reference interface="com.example.demo.SampleService" id="sampleService"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;sofa:global-attrs type="future"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:reference&gt;</pre>
<p>注解配置： </p>
<pre class="crayon-plain-tag">@SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", invokeType = "future"))
private SampleService sampleService;</pre>
<p>编程式，使用Spring的情况下： </p>
<pre class="crayon-plain-tag">BoltBindingParam boltBindingParam = new BoltBindingParam();
boltBindingParam.setType("future");</pre>
<p>编程式，不使用Spring的情况下： </p>
<pre class="crayon-plain-tag">ConsumerConfig consumerConfig = new ConsumerConfig()
    .setInterfaceId(SampleService.class.getName())
    .setRegistry(registryConfig)
    .setProtocol("bolt")
    .setInvokeType("future");</pre>
<p>要获得异步响应，调用：</p>
<pre class="crayon-plain-tag">// 第一个参数为超时，第二个参数提示是否删除线程上下文中的调用结果缓存
String result = (String)SofaResponseFuture.getResponse(0, true);</pre>
<p>JDK的Future对象也可以得到：</p>
<pre class="crayon-plain-tag">Future future = SofaResponseFuture.getFuture(true);</pre>
</td>
</tr>
<tr>
<td>Callback</td>
<td>
<p>调用类型名称：callback
<p>类似Asynchronous，不需要等待结果。当结果到达后，自动调用注册的回调函数。你需要实现如下的回调接口：</p>
<pre class="crayon-plain-tag">/**
 * 面向用户的Rpc请求结果监听器
 *
 */
public interface SofaResponseCallback {
    /**
     * SOFA RPC 会在调用成功的响应到达后回调此方法
     *
     * @param appResponse 响应对象
     * @param methodName 被调用的方法
     * @param request 请求对象
     */
    void onAppResponse(Object appResponse, String methodName, RequestBase request);

    /**
     * SOFA RPC 会在服务器端异常时回调此方法
     */
    void onAppException(Throwable throwable, String methodName, RequestBase request);

    /**
     * SOFA RPC 会在框架异常时回调此方法
     *
     */
    void onSofaException(SofaRpcException sofaException, String methodName, RequestBase request);
}</pre>
<p>然后，你可以使用下面的方式注册回调接口。</p>
<p>XML方式：</p>
<pre class="crayon-plain-tag">&lt;!-- 回调Bean --&gt;
&lt;bean id="sampleCallback" class="com.example.demo.SampleCallback"/&gt;
&lt;sofa:reference interface="com.example.demo.SampleService" id="sampleService"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;!--                               引用回调Bean                 --&gt;
        &lt;sofa:global-attrs type="callback" callback-ref="sampleCallback"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:reference&gt;</pre>
<p>注解方式：</p>
<pre class="crayon-plain-tag">@SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt",
            invokeType = "callback",
            callbackRef = "sampleCallback"))
private SampleService sampleService;</pre>
<p> 编程式，使用Spring的情况下： </p>
<pre class="crayon-plain-tag">BoltBindingParam boltBindingParam = new BoltBindingParam();
boltBindingParam.setType("callback");
boltBindingParam.setCallbackClass("com.example.demo.SampleCallback");</pre>
<p> 编程式，不使用Spring的情况下：  </p>
<pre class="crayon-plain-tag">ConsumerConfig consumerConfig = new ConsumerConfig()
    .setInterfaceId(SampleService.class.getName())
    .setRegistry(registryConfig)
    .setProtocol("bolt")
    .setInvokeType("callback")
    .setOnReturn(new SampleCallback());</pre>
<p>你还可以在调用期间临时的设置：</p>
<pre class="crayon-plain-tag">RpcInvokeContext.getContext().setResponseCallback(new SampleCallback());</pre>
</td>
</tr>
<tr>
<td>Oneway</td>
<td>
<p>调用类型名称：oneway
<p>发送请求后就不管了，不在乎结果如何的情况下使用</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">超时控制</span></div>
<p>使用Bolt协议时默认的超时是3s。你可以在多个级别设置超时。</p>
<p>在服务级别设置：</p>
<pre class="crayon-plain-tag">&lt;sofa:reference interface="com.example.demo.SampleService" id="sampleService"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;sofa:global-attrs timeout="2000"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:reference&gt;</pre><br />
<pre class="crayon-plain-tag">@SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", timeout = 2000)) 
private SampleService sampleService;</pre>
<p>在方法级别设置： </p>
<pre class="crayon-plain-tag">&lt;sofa:reference interface="com.example.demo.SampleService" id="sampleService"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;sofa:method name="hello" timeout="2000"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:reference&gt;</pre>
<div class="blog_h3"><span class="graybg">泛化调用</span></div>
<p>SOFARPC允许消费者在不知道服务接口的情况下发起调用。前提条件是：</p>
<ol>
<li>使用Bolt作为通信协议</li>
<li>使用Hessian 2作为串行化协议 </li>
</ol>
<div class="blog_h3"><span class="graybg">串行化协议</span></div>
<p>SOFARPC支持Hessian 2、protobuf两个串行化协议，默认使用前者。如果要修改，参考：</p>
<pre class="crayon-plain-tag">sofa:service ref="sampleService" interface="com.alipay.sofarpc.demo.SampleService"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;sofa:global-attrs serialize-type="protobuf"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:service&gt;
&lt;!-- 提供者/消费者都需要设置 --&gt;
&lt;sofa:reference interface="com.alipay.sofarpc.demo.SampleService" id="sampleServiceRef" jvm-first="false"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;sofa:global-attrs serialize-type="protobuf"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:reference&gt;</pre>
<div class="blog_h3"><span class="graybg">定制线程池</span></div>
<pre class="crayon-plain-tag">&lt;bean id="helloService" class="com.alipay.sofa.rpc.quickstart.HelloService"/&gt;

&lt;!-- 自定义一个线程池 --&gt;
&lt;bean id="customExecutor" class="com.alipay.sofa.rpc.server.UserThreadPool" init-method="init"&gt;
    &lt;property name="corePoolSize" value="10" /&gt;
    &lt;property name="maximumPoolSize" value="10" /&gt;
    &lt;property name="queueSize" value="0" /&gt;
&lt;/bean&gt;

&lt;sofa:service ref="helloService" interface="XXXService"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;!-- 引用线程池 --&gt;
        &lt;sofa:global-attrs thread-pool-ref="customExecutor"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:service&gt;</pre>
<p>或者，使用注解方式：</p>
<pre class="crayon-plain-tag">@SofaService(bindings = {@SofaServiceBinding(bindingType = "bolt", userThreadPool = "customThreadPool")})
public class SampleServiceImpl implements SampleService {
}</pre>
<div class="blog_h2"><span class="graybg">RESTful协议 </span></div>
<p>SOFARPC支持RESTful协议，使用此协议时，你需要为服务接口添加JAX-RS注解：</p>
<pre class="crayon-plain-tag">@Path("sample")
public interface SampleService {
    @GET
    @Path("hello")
    String hello();
}</pre>
<p>发布服务的方式如下：</p>
<pre class="crayon-plain-tag">@Service
//                                           设置绑定类型
@SofaService(bindings = {@SofaServiceBinding(bindingType = "rest")})
public class RestfulSampleServiceImpl implements SampleService {
    @Override
    public String hello() {
        return "Hello";
    }
}</pre>
<p>这种服务直接可以通过浏览器访问： http://localhost:8341/sample/hello</p>
<p>在SOFARPC客户端，消费服务的方式如下：</p>
<pre class="crayon-plain-tag">@SofaReference(binding = @SofaReferenceBinding(bindingType = "rest"))
private SampleService sampleService;</pre>
<div class="blog_h2"><span class="graybg">注册中心 </span></div>
<p>SOFARPC支持多种注册中心。当前bolt、rest、duboo传输协议均支持ZooKeeper作为注册中心，bolt、rest还支持本地文件系统作为注册中心（主要用于测试）。</p>
<div class="blog_h3"><span class="graybg">SOFARegistry</span></div>
<p>当前SOFARPC（SOFARPC: 5.5.2, SOFABoot: 2.6.3）已经支持SOFARegistry注册中心：</p>
<pre class="crayon-plain-tag">com.alipay.sofa.rpc.registry.address=sofa://127.0.0.1:9603</pre>
<div class="blog_h3"><span class="graybg">Zookeeper</span></div>
<p>要使用此注册中心，配置：</p>
<pre class="crayon-plain-tag">com.alipay.sofa.rpc.registry.address=zookeeper://127.0.0.1:2181
# 指定身份验证信息
com.alipay.sofa.rpc.registry.address=zookeeper://xxx:2181?file=/home/admin/registry&amp;scheme=digest&amp;addAuth=sofazk:rpc1</pre>
<div class="blog_h3"><span class="graybg">本地文件系统 </span></div>
<p>要使用此注册中心，配置：</p>
<pre class="crayon-plain-tag">com.alipay.sofa.rpc.registry.address=local:///home/admin/registry/localRegistry.reg</pre>
<div class="blog_h2"><span class="graybg">直接调用</span></div>
<p>服务消费者可以直接指定服务提供者的地址： </p>
<pre class="crayon-plain-tag">ConsumerConfig consumer = new ConsumerConfig()        
            .setInterfaceId(HelloService.class.getName())        
            .setRegistry(registryConfig)        
            .setDirectUrl("bolt://127.0.0.1:12201");</pre><br />
<pre class="crayon-plain-tag">&lt;sofa:reference interface="com.alipay.sample.HelloService" id="helloService"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;sofa:route target-url="127.0.0.1:12200"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:reference&gt;</pre><br />
<pre class="crayon-plain-tag">@SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", directUrl = "127.0.0.1:12220"))
private SampleService sampleService;</pre>
<p>而不经过SOFARPC的负载均衡系统。</p>
<div class="blog_h2"><span class="graybg">负载均衡 </span></div>
<p>目前支持的负载均衡算法：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">算法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>random</td>
<td>随即选取服务提供者，默认</td>
</tr>
<tr>
<td>localPref</td>
<td>如果存在本地可用的服务提供者，优先使用之</td>
</tr>
<tr>
<td>roundRobin</td>
<td>轮询算法</td>
</tr>
<tr>
<td>consistentHash</td>
<td>一致性哈希算法，每个相同方法级别的请求路由到同一节点</td>
</tr>
<tr>
<td>weightRoundRobin</td>
<td>加权的轮询</td>
</tr>
</tbody>
</table>
<p>配置负载均衡算法的方式如下： </p>
<pre class="crayon-plain-tag">&lt;sofa:reference interface="com.example.demo.SampleService" id="sampleService"&gt;
    &lt;sofa:binding.bolt&gt;
        &lt;sofa:global-attrs loadBalancer="roundRobin"/&gt;
    &lt;/sofa:binding.bolt&gt;
&lt;/sofa:reference&gt;</pre>
<div class="blog_h2"><span class="graybg">重试</span></div>
<p>重试的配置方式如下：</p>
<pre class="crayon-plain-tag">&lt;sofa:reference jvm-first="false" id="retriesServiceReferenceBolt" interface="com.alipay.sofa.rpc.samples.retries.RetriesService"&gt;
   &lt;sofa:binding.bolt&gt;
     &lt;sofa:global-attrs retries="2"/&gt;
   &lt;/sofa:binding.bolt&gt;
&lt;/sofa:reference&gt;</pre><br />
<pre class="crayon-plain-tag">@SofaReference(binding = @SofaReferenceBinding(bindingType = "bolt", retries = 2))
private SampleService sampleService;</pre>
<div class="blog_h2"><span class="graybg">分布式追踪</span></div>
<p>SOFARPC目前支持以下Tracer：</p>
<ol>
<li>SOFATracer，内置集成</li>
<li>Skywalking</li>
</ol>
<p>如果要禁用分布式追踪特性，可以配置：</p>
<pre class="crayon-plain-tag">com.alipay.sofa.rpc.defaultTracer=</pre>
<div class="blog_h2"><span class="graybg">容错性</span></div>
<p>SOFARPC支持内置的容错机制，还可以和Hystrix进行集成。</p>
<div class="blog_h3"><span class="graybg">自动剔除</span></div>
<p>运行机制：</p>
<ol>
<li>单机故障剔除会<span style="background-color: #c0c0c0;">统计一个时间窗口内的调用次数和异常次数</span>，并计算<span style="background-color: #c0c0c0;">每个服务对应ip的异常率和该服务的平均异常率</span></li>
<li>当达到ip<span style="background-color: #c0c0c0;">异常率大于服务平均异常率到一定比例</span>时，会对<span style="background-color: #c0c0c0;">服务+ip的维度进行权重降级</span></li>
<li>如果该服务+ip维度的<span style="background-color: #c0c0c0;">权重并没有降为0，那么当该服务+ip维度的调用情况正常时，则会对其进行权重恢复</span></li>
<li>整个计算和调控过程异步进行，不会阻塞调用</li>
</ol>
<p>使用这种单机故障剔除，需要进行如下配置：</p>
<pre class="crayon-plain-tag">FaultToleranceConfig faultToleranceConfig = new FaultToleranceConfig();
        faultToleranceConfig.setRegulationEffective(true);
        faultToleranceConfig.setDegradeEffective(true);
        // 时间窗口 20s
        faultToleranceConfig.setTimeWindow(20);
        // 如果被判定为故障，则权重掉1/2
        faultToleranceConfig.setWeightDegradeRate(0.5);

FaultToleranceConfigManager.putAppConfig("appName", faultToleranceConfig);</pre>
<div class="blog_h3"><span class="graybg">集成Hystrix</span></div>
<p>基于Hystrix的熔断能力，目前还不健全。要使用此特性，添加Hystrix的依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
        &lt;groupId&gt;com.netflix.hystrix&lt;/groupId&gt;
        &lt;artifactId&gt;hystrix-core&lt;/artifactId&gt;
        &lt;version&gt;1.5.12&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>然后显式启用Hystrix（导致Hystrix过滤器被加载）： </p>
<pre class="crayon-plain-tag">// 全局开启
RpcConfigs.putValue(HystrixConstants.SOFA_HYSTRIX_ENABLED, true);

// 对特定 Consumer 开启
ConsumerConfig consumerConfig = new ConsumerConfig()
        .setInterfaceId(HelloService.class.getName())
        .setParameter(HystrixConstants.SOFA_HYSTRIX_ENABLED, String.valueOf(true));</pre>
<p>FallbackFactory接口用于注入Hystrix的Fallback，当Hystrix遇到异常、超时、线程池拒绝、熔断等情况时，指定执行此Fallback（降级）逻辑：</p>
<pre class="crayon-plain-tag">// 可以直接使用默认的 FallbackFactory 直接注入 Fallback 实现
SofaHystrixConfig.registerFallback(consumerConfig, new HelloServiceFallback());

// 也可以自定义 FallbackFactory 直接注入 FallbackFactory
SofaHystrixConfig.registerFallbackFactory(consumerConfig, new HelloServiceFallbackFactory());</pre>
<div class="blog_h2"><span class="graybg">优雅关闭</span></div>
<p>SOFARPC注册了JVM的ShutdownHook，用于实现优雅的资源清理。 </p>
<div class="blog_h1"><span class="graybg">SOFARegistry</span></div>
<p>这是一个服务注册中心，和ZooKeeper、Etcd等项目对比有自己的特点：</p>
<ol>
<li>高度可扩容：数据分片存储，理论上可以支持无限水平扩容</li>
<li>低延迟：基于SOFABolt框架，实现TCP长连接下的变更推送。主流注册中心都是这种架构</li>
<li>高可用：在CAP中保证AP，<span style="background-color: #c0c0c0;">放弃严格一致性，尽可能在网络分区时保证可用性</span>。这和ZooKeeper、Etcd不同。需要注意的是，<span style="background-color: #c0c0c0;">Envoy xDS协议也是最终一致性的</span></li>
</ol>
<p>在架构上，SOFARegistry引入4种角色：</p>
<ol>
<li>Client：通过客户端JAR包接入注册中心</li>
<li>SessionServer：直接处理Client的服务发布/订阅请求，可以无限扩容。<span style="background-color: #c0c0c0;">客户端仅和SessionServer交互</span></li>
<li>DataServer：负责存储服务的元数据，使用一致性哈希分片存储，支持多副本，可以无限扩容</li>
<li>MetaServer：维护SessionServer、DataServer集群的一致性列表，在节点发生变更时发出通知</li>
</ol>
<div class="blog_h2"><span class="graybg">术语列表</span></div>
<div class="blog_h3"><span class="graybg">RPC通用术语</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>服务<br />Service</td>
<td>通过网络提供的、具有特定业务逻辑处理能力的软件功能</td>
</tr>
<tr>
<td>服务提供者<br />Service Provider</td>
<td>通过网络提供服务的计算机节点</td>
</tr>
<tr>
<td>服务消费者<br />Service Consumer</td>
<td>通过网络调用服务的计算机节点。一个计算机节点可以既作为一些服务的提供者，又作为一些服务的消费者</td>
</tr>
<tr>
<td>服务发现<br />Service Discovery</td>
<td>服务消费者获取服务提供者的网络地址的过程</td>
</tr>
<tr>
<td>服务注册中心<br />Service Registry</td>
<td>一种提供服务发现功能的软件系统，帮助服务消费者获取服务提供者的网络地址</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg"> 专门术语</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>数据（Data）</td>
<td>在服务发现场景下，特指服务提供者的网络地址及其它附加信息。其他场景下，也可以表示任意发布到 SOFARegistry 的信息</td>
</tr>
<tr>
<td>单元（Zone）</td>
<td>单元化架构关键概念，在服务发现场景下，单元是一组发布与订阅的集合，发布及订阅服务时需指定单元名</td>
</tr>
<tr>
<td>发布者（Publisher）</td>
<td>发布数据到 SOFARegistry 的节点。在服务发现场景下，服务提供者就是“服务提供者的网络地址及其它附加信息”的发布者</td>
</tr>
<tr>
<td>订阅者（Subscriber）</td>
<td>从 SOFARegistry 订阅数据的节点。在服务发现场景下，服务消费者就是“服务提供者的网络地址及其它附加信息”的订阅者</td>
</tr>
<tr>
<td>数据标识（DataId）</td>
<td>用来标识数据的字符串。在服务发现场景下，通常由服务接口名、协议、版本号等信息组成，作为服务的标识</td>
</tr>
<tr>
<td>分组标识（GroupId）</td>
<td>用于为数据归类的字符串，可以作为数据标识的命名空间，即只有 DataId、GroupId、InstanceId 都相同的服务，才属于同一服务</td>
</tr>
<tr>
<td>实例 ID（InstanceId）</td>
<td>实例 ID，可以作为数据标识的命名空间，即只有DataId、GroupId、InstanceId都相同的服务，才属于同一服务</td>
</tr>
<tr>
<td>会话服务器（SessionServer）</td>
<td>SOFARegistry 内部负责跟客户端建立 TCP 长连接、进行数据交互的一种服务器角色</td>
</tr>
<tr>
<td>数据服务器（DataServer）</td>
<td>SOFARegistry 内部负责数据存储的一种服务器角色。</td>
</tr>
<tr>
<td>元信息服务器（MetaServer）</td>
<td>SOFARegistry 内部基于 Raft 协议，负责集群内一致性协调的一种服务器角色。</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">起步</span></div>
<div class="blog_h3"><span class="graybg">部署服务器</span></div>
<p>支持独立部署、集成部署方式。后者比较简单，单节点部署，可以用于测试。</p>
<pre class="crayon-plain-tag">wget https://github.com/alipay/sofa-registry/releases/download/v5.2.0/registry-integration-5.2.0.tar.gz
mkdir registry-integration
tar -zxvf registry-integration-5.2.0.tar.gz -C registry-integration
cd registry-integration

# 启动脚本位于
bin/startup.sh</pre>
<p>启动服务器后，执行下面的命令查看各服务器端角色的健康状况：</p>
<pre class="crayon-plain-tag"># 查看meta角色的健康检测接口：
curl http://localhost:9615/health/check
# {"success":true,"message":"... raftStatus:Leader"}

# 查看data角色的健康检测接口：
curl http://localhost:9622/health/check
# {"success":true,"message":"... status:WORKING"}

# 查看session角色的健康检测接口：
curl http://localhost:9603/health/check
# {"success":true,"message":"..."}</pre>
<div class="blog_h3"><span class="graybg">使用客户端</span></div>
<p>引入依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;    
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;registry-client-all&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
<p>下面的代码演示了如何发布数据到SOFARegistry上： </p>
<pre class="crayon-plain-tag">// 构建客户端实例
RegistryClientConfig config = DefaultRegistryClientConfigBuilder.start()
    // 服务器连接地址，任意一个Session节点都可以
    .setRegistryEndpoint("127.0.0.1").setRegistryEndpointPort(9603).build();
DefaultRegistryClient registryClient = new DefaultRegistryClient(config);
registryClient.init();

// 构造发布者注册表
// dataId是此客户端发布的服务的唯一标识
String dataId = "com.alipay.test.demo.service:1.0@DEFAULT";
PublisherRegistration registration = new PublisherRegistration(dataId);

// 将注册表注册进客户端并发布数据
registryClient.register(registration, "10.10.1.1:12200?xx=yy");</pre>
<p>下面的代码演示了如何从SOFARegistry订阅数据：</p>
<pre class="crayon-plain-tag">// 构建客户端实例
RegistryClientConfig config = DefaultRegistryClientConfigBuilder.start()
    .setRegistryEndpoint("127.0.0.1").setRegistryEndpointPort(9603).build();
DefaultRegistryClient registryClient = new DefaultRegistryClient(config);
registryClient.init();

// 创建 SubscriberDataObserver 
SubscriberDataObserver subscriberDataObserver = new SubscriberDataObserver() {
  	public void handleData(String dataId, UserData userData) {
                // 在这里处理接收到的数据
    		System.out.println("receive data success, dataId: " + dataId + ", data: " + userData);
  	}
};

// 构造订阅者注册表
String dataId = "com.alipay.test.demo.service:1.0@DEFAULT";
SubscriberRegistration registration = new SubscriberRegistration(dataId, subscriberDataObserver);
// 订阅的范围：zone, dataCenter, global
registration.setScopeEnum(ScopeEnum.global);

// 将注册表注册进客户端并订阅数据，订阅到的数据会以回调的方式通知 SubscriberDataObserver
registryClient.register(registration);</pre>
<p>有数据更新后，服务器会推送并回调，你需要在回调中处理 UserData：</p>
<pre class="crayon-plain-tag">public interface UserData {
    // 以Zone分组的数据
    Map&lt;String, List&gt; getZoneData();
    // 当前Zone
    String getLocalZone();
}</pre>
<div class="blog_h1"><span class="graybg">SOFATracer</span></div>
<p>遵循OpenTracing规范的Tracer。可以将将一个Trace的链路信息打印到日志或发送给Zipkin进行展示。 特性包括：</p>
<ol>
<li>基于<a href="https://github.com/LMAX-Exchange/disruptor">Disruptor</a>实现高性能的Trace日志落盘。支持两种打印类型：
<ol>
<li>摘要日志：每一次调用均会落地磁盘的日志</li>
<li>统计日志：每隔一定时间间隔进行统计输出的日志</li>
</ol>
</li>
<li>支持日志自清除和轮换</li>
<li>集成SLF4J的MDC，修改日志配置即可输出当前Trace上下文的TraceId 和 SpanId</li>
<li>已开发多种开源项目的埋点：
<ol>
<li>Spring MVC</li>
<li>基于标准JDBC接口的数据库连接池，包括DBCP、Druid、c3p0、tomcat、HikariCP、BoneCP</li>
<li>HttpClient</li>
<li>RestTemplate</li>
<li>OkHttp</li>
<li>Dubbo</li>
<li>OpenFeign</li>
<li>Redis、消息中间件的埋点仍然在开发中 </li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">术语列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>TraceId</td>
<td>
<p>在SOFATracer 中代表一次请求的唯一标识，此 ID 一般<span style="background-color: #c0c0c0;">由集群中第一个处理请求的系统产生</span>，并在分布式调用下通过网络传递到下一个被请求系统</p>
<p>TraceId的示例：</p>
<pre class="crayon-plain-tag"># 启动Trace的那个服务器的IP地址，十六进制
#        Trace产生的时间戳
#                      自增序列
#                           当前进程ID   
0ad1348f 1403169275002 1003 56696</pre>
</td>
</tr>
<tr>
<td>SpanId</td>
<td>SpanId 代表了本次请求在整个调用链路中的位置或者说层次，比如 A 系统在处理一个请求的过程中依次调用了 B，C，D 三个系统，那么这三次调用的的 SpanId 分别是：0.1，0.2，0.3。如果 B 系统继续调用了 E，F 两个系统，那么这两次调用的 SpanId 分别是：0.1.1，0.1.2</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">配置</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 38%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>logging.path</td>
<td>
<p>日志输出目录
<p>SOFATracer 会优先输出到 logging.path 目录下；如果没有配置日志输出目录，那默认输出到 ${user.home}</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.disableDigestLog</td>
<td>
<p>是否关闭所有集成 SOFATracer 组件摘要日志打印</p>
<p>默认false</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.disableConfiguration[${logType}]</td>
<td>
<p>关闭指定 ${logType} 的 SOFATracer 组件摘要日志打印。${logType}是指具体的日志类型，如：spring-mvc-digest.log</p>
<p>默认false</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.tracerGlobalRollingPolicy</td>
<td>
<p>SOFATracer 日志的滚动策略</p>
<p>.yyyy-MM-dd：按照天滚动，默认<br />.yyyy-MM-dd_HH：按照小时滚动</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.tracerGlobalLogReserveDay</td>
<td>
<p>SOFATracer 日志的保留天数</p>
<p>默认保留 7 天</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.statLogInterval</td>
<td>
<p>统计日志的时间间隔，单位：秒</p>
<p>默认 60 秒统计日志输出一次</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.baggageMaxLength</td>
<td>
<p>透传数据能够允许存放的最大长度</p>
<p>默认值 1024</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.zipkin.enabled</td>
<td>
<p>是否开启 SOFATracer 远程上报数据到 Zipkin</p>
<p>true：开启上报；false：关闭上报。默认不上报</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.zipkin.baseUrl</td>
<td>
<p>SOFATracer 远程上报数据到 Zipkin 的地址，com.alipay.sofa.tracer.zipkin.enabled=true时配置此地址才有意义</p>
<p>格式：http://${host}:${port}</p>
</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.springmvc.filterOrder</td>
<td>SOFATracer 集成在 SpringMVC 的 Filter 生效的 Order</td>
</tr>
<tr>
<td>com.alipay.sofa.tracer.springmvc.urlPatterns</td>
<td>
<p>SOFATracer 集成在 SpringMVC 的 Filter 生效的 URL Pattern 路径</p>
<p>默认/*，全部生效</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">埋点植入</span></div>
<p>&nbsp;</p>
<div class="blog_h2"><span class="graybg">SLF4J集成</span></div>
<p>SLF4J 提供了 MDC （Mapped Diagnostic Contexts）功能，支持用户定义和修改日志的输出格式以及内容。SOFATracer支持基于MDC来输出当前Trace上下文的TraceId、SpanId。</p>
<p>需要引入SLF4J的API，以及日志实现包的Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.slf4j&lt;/groupId&gt;
    &lt;artifactId&gt;slf4j-api&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;!-- Logback --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-logging&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;!-- Log4j2 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-log4j2&lt;/artifactId&gt;
    &lt;!--SOFABoot没有管控log4j2 版本 --&gt;
    &lt;version&gt;1.4.2.RELEASE&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>配置日志输出的PatternLayout：</p>
<pre class="crayon-plain-tag">&lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} %5p  [%X{SOFA-TraceId}, %X{SOFA-SpanId}] -- %m%n&lt;/pattern&gt;</pre>
<p>%X表示，在调用appender输出日志时，将此占位符替换为当前线程上下文（MDC）中的SOFA-TraceId、SOFA-SpanId变量。</p>
<div class="blog_h2"><span class="graybg">异步传递追踪上下文</span></div>
<div class="blog_h3"><span class="graybg">Runnable</span></div>
<p>如果用户启动新线程来处理业务，则基于线程本地变量传递的Trace信息就丢失了。为了将SOFATracer日志上下文从父线程传递到子线程，可以考虑用SofaTracerRunnable：</p>
<pre class="crayon-plain-tag">//                         使用SOFATracer提供的包装器
Thread thread = new Thread(new SofaTracerRunnable(new Runnable() {
            @Override
            public void run() {
                // 异步业务逻辑
            }
        }));
thread.start();</pre>
<div class="blog_h3"><span class="graybg">Callable</span></div>
<p>基于java.util.concurrent.Callable发动新线程时类似：</p>
<pre class="crayon-plain-tag">ExecutorService executor = Executors.newCachedThreadPool();
//                                                            使用SOFATracer提供的包装器
SofaTracerCallable&lt;Object&gt; sofaTracerSpanSofaTracerCallable = new SofaTracerCallable&lt;Object&gt;(new Callable&lt;Object&gt;() {
    @Override
    public Object call() throws Exception {
        // 异步业务逻辑
        return ...;
    }
});
Future&lt;Object&gt; futureResult = executor.submit(sofaTracerSpanSofaTracerCallable);
// ...
Object objectReturn = futureResult.get();</pre>
<div class="blog_h2"><span class="graybg">采样模式</span></div>
<p>SOFATracer支持两种采样模式：</p>
<ol>
<li> 基于BitSet实现的固定采样率的采样模式</li>
<li>用户自定义实现采样的采样模式</li>
</ol>
<div class="blog_h3"><span class="graybg">固定采样率</span></div>
<p>进行以下配置即可：</p>
<pre class="crayon-plain-tag"># 采样率0-100之间
com.alipay.sofa.tracer.samplerPercentage=100
# 采样模式类型名称
com.alipay.sofa.tracer.samplerName=PercentageBasedSampler</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">SOFALookout</span></div>
<p>此项目解决的是监控领域的问题，提供指标的埋点、收集、加工、存储、查询等服务，分为客户端、服务器两个部分。</p>
<p>此项目已经快一年没有更新。</p>
<div class="blog_h1"><span class="graybg">SOFAMesh</span></div>
<p>扩展了Istio项目，并做了以下改进：</p>
<ol>
<li>将数据平面的Envoy替换为SOFAMosn</li>
<li><span style="background-color: #c0c0c0;">下沉Mixer的功能到数据平面，提升性能</span></li>
<li>扩展Pilot，支持更多的服务发现机制（服务注册表），包括SOFARPC、Dubbo</li>
</ol>
<p>SOFARPC、Dubbo之类的入侵式框架可以在Istio中运行，但是无法对其进行任何管理、监控。SOFAMesh解决了这些痛点。</p>
<div class="blog_h1"><span class="graybg">SOFAMosn</span></div>
<p>MOSN（Modular Observable Smart Network，模块化可观察智能网络…）是Envoy的替代品，其出现的主要原因不是Envoy不行，而是阿里系不愿引入C++技术栈。</p>
<p>MOSN增加了对SOFARPC、Dubbo协议的支持，后者仍然在开发中。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/sofastack-study-note">SOFAStack学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/sofastack-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>扩展Istio</title>
		<link>https://blog.gmem.cc/extend-istio</link>
		<comments>https://blog.gmem.cc/extend-istio#comments</comments>
		<pubDate>Mon, 25 Mar 2019 07:42:09 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=26071</guid>
		<description><![CDATA[<p>前言 如果Istio不能满足你的需求，你可以考虑扩展它。 Pilot的功能相对比较固定，主要负责和Envoy代理基于xDS协议的数据交换，通常不需要进行扩展和定制。 Mixer本身即是高度模块化、并且鼓励扩展的。我们可以定义自己的模板，从网格流量中抽取新的属性，也可以开发自己的适配器，来支持和各种后端基础设施的对接。本文的主要篇幅将用来探讨Mixer的扩展。 如何贡献 Istio开发所需的资源参考官方Wiki。 前提条件 在进行Istio开发之前，先准备好： Go 1.11版本 Docker，Istio包含一个镜像构建系统，能够创建、发布Docker镜像 如果在K8S环境下运行Istio，你需要K8S 1.7.3以上版本 签出源码 [crayon-69d3148a69bbf078858063/] 环境变量 [crayon-69d3148a69bc4406216672/] 构建Istio 在本机环境下构建： [crayon-69d3148a69bc6307505103/] 构建并打包到容器： [crayon-69d3148a69bc8419830972/] 调试Istio <a class="read-more" href="https://blog.gmem.cc/extend-istio">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-istio">扩展Istio</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>如果Istio不能满足你的需求，你可以考虑扩展它。</p>
<p>Pilot的功能相对比较固定，主要负责和Envoy代理基于xDS协议的数据交换，通常不需要进行扩展和定制。</p>
<p>Mixer本身即是高度模块化、并且鼓励扩展的。我们可以定义自己的模板，从网格流量中抽取新的属性，也可以开发自己的适配器，来支持和各种后端基础设施的对接。本文的主要篇幅将用来探讨Mixer的扩展。</p>
<div class="blog_h1"><span class="graybg">如何贡献</span></div>
<p>Istio开发所需的资源参考<a href="https://github.com/istio/istio/wiki">官方Wiki</a>。</p>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<p>在进行Istio开发之前，先准备好：</p>
<ol>
<li>Go 1.11版本</li>
<li>Docker，Istio包含一个镜像构建系统，能够创建、发布Docker镜像</li>
<li>如果在K8S环境下运行Istio，你需要K8S 1.7.3以上版本</li>
</ol>
<div class="blog_h2"><span class="graybg">签出源码</span></div>
<pre class="crayon-plain-tag"># 必须签出到$GOPATH/src/istio.io下
pushd /home/alex/Go/workspaces/default/src/istio.io
git clone https://github.com/istio/istio.git</pre>
<div class="blog_h2"><span class="graybg">环境变量</span></div>
<pre class="crayon-plain-tag">export GOPATH=~/go
export PATH=$PATH:$GOPATH/bin
export ISTIO=$GOPATH/src/github.com/istio/istio

# Docker镜像仓库和Tag
export HUB="docker.gmem.cc/istio"
export TAG=1.0.5

export KUBECONFIG=${HOME}/.kube/config </pre>
<div class="blog_h2"><span class="graybg">构建Istio</span></div>
<p>在本机环境下构建：</p>
<pre class="crayon-plain-tag"># 基于本机的体系结构构建Istio的Pilot、Mixer、Citadel等组件
make

# 构建包含调试信息的组件，可以基于Delve等Debugger进行单步调试
make DEBUG=1

# 提升非第一次构建的速度，-i让Go缓存中间结果
GOBUILDFLAGS=-i make</pre>
<p>构建并打包到容器：</p>
<pre class="crayon-plain-tag">make docker
make DEBUG=1 docker

# 推送到镜像仓库
make push</pre>
<div class="blog_h2"><span class="graybg">调试Istio</span></div>
<div class="blog_h3"><span class="graybg">本地</span></div>
<p>你可以参考下面的命令在本地启动Pilot：</p>
<pre class="crayon-plain-tag">... discovery --log_output_level=default:debug --domain=k8s.gmem.cc --kubeconfig=/home/alex/.kube/config --meshConfig=pilot/mesh </pre>
<div class="blog_h3"><span class="graybg">K8S</span></div>
<p>在K8S环境下，调试Istio容器的步骤如下：</p>
<ol>
<li>定位到Istio容器在什么节点下运行</li>
<li>确保必要的工具都在节点上安装好，包括Go、Delve</li>
<li>将可执行文件基于的源码拷贝到节点上</li>
<li>在节点上找到Istio容器对应的进程</li>
<li>执行<pre class="crayon-plain-tag">sudo dlv attach pilot-pid</pre>开始调试 </li>
</ol>
<p>你也可以使用Squash配合Delve进行调试，可能需要修改Istio的基础镜像（例如alpine）。使用Squash的优势是不需要在所有节点上都安装Delve+Go</p>
<div class="blog_h3"><span class="graybg">连接到本地</span></div>
<p>给Deployment增加注解：<pre class="crayon-plain-tag">sidecar.istio.io/discoveryAddress: 10.0.0.1:15010</pre> 即可强制指定Pilot的地址。</p>
<div class="blog_h2"><span class="graybg">运行测试</span></div>
<pre class="crayon-plain-tag"># 运行所有测试
make test

# 运行Pilot的单元测试
make pilot-test

# 使用Go竞态条件检测工具运行测试
make racetest


# 获取测试覆盖率信息
make coverage</pre>
<div class="blog_h3"><span class="graybg">测试和PR</span></div>
<p>只有通过单元测试、集成测试之后，才能提交PR，否则不会被合并：</p>
<ol>
<li>单元测必须是密封的。仅仅访问test binary中的资源</li>
<li>所有包、重要文件必须进行单元测试</li>
<li>单元测试使用标准的Go测试包</li>
<li>测试多种场景/输入时，最好使用<a href="https://github.com/golang/go/wiki/TableDrivenTests">表驱动测试</a></li>
<li>必须通过并发测试</li>
</ol>
<div class="blog_h2"><span class="graybg">格式化代码</span></div>
<pre class="crayon-plain-tag">make format</pre>
<div class="blog_h2"><span class="graybg">代码检查</span></div>
<pre class="crayon-plain-tag">make lint

# 仅仅针对本地变更进行检查
bin/linters.sh -s HEAD^</pre>
<div class="blog_h2"><span class="graybg">使用CircleCI </span></div>
<p>Istio使用CircleCI作为持续集成系统，所有PR必须通过全部CircleCI测试才能被合并。当Fork了Istio之后，CircleCI测试环境也被复制到本地，可以完整重现Istio的测试基础设施。</p>
<p>你可以注册CircleCI账号，并在Fork中测试代码的变更，防止PR不被通过。</p>
<div class="blog_h2"><span class="graybg">Git工作流</span></div>
<ol>
<li>Fork主仓库</li>
<li>克隆Fork到本地</li>
<li>启用提交前钩子：<pre class="crayon-plain-tag">./bin/pre-commit</pre></li>
<li> 创建一个分支，修改一些代码</li>
<li>保持Fork和主仓库同步：<br />
<pre class="crayon-plain-tag">git fetch upstream
git rebase upstream/master</pre>
</li>
<li>
<p>提交变更到Fork</p>
</li>
<li>
<p>推送变更到Fork</p>
</li>
<li>
<p>创建一个PR</p>
</li>
<li>PR会分配给1-N个reviewer，他们会检查代码、文档、注释，包括代码样式</li>
</ol>
<div class="blog_h2"><span class="graybg">在特性分支上开发</span></div>
<p>开发新的试验特性，或者进行可能对master稳定性造成巨大影响的变更时，应当新开启特性分支，并遵守：</p>
<ol>
<li>以<pre class="crayon-plain-tag">collab-&lt;feature-name&gt;</pre>的方式命名分支</li>
<li>周期性的从master合并代码，长期不合并，导致最终将特性分支合并到master时非常困难</li>
</ol>
<div class="blog_h2"><span class="graybg">Istio测试框架</span></div>
<p>让用测试例本身快速、可靠的基于云环境运行是困难的，Istio测试框架尝试解决该问题。</p>
<p>Istio测试框架的目标：</p>
<ol>
<li>编写测试：
<ol>
<li>平台不可知：API将底层平台的细节屏蔽掉，让开发人员专注于测试Istio本身的裸机</li>
<li>可重用测试：可以基于任何支持Istio的底层平台运行测试</li>
</ol>
</li>
<li>运行测试：
<ol>
<li>基于Go语言标准测试机制</li>
<li>简单：不需要或需要很少的标记即可运行测试</li>
<li>快速</li>
<li>可靠：在本机运行测试天然比在集群中可靠，但是针对各平台的组件都具有可靠性机制，例如重试</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">起步</span></div>
<p>使用此测试框架，你需要编写一个TestMain函数：</p>
<pre class="crayon-plain-tag">func TestMain(m *testing.M) { 
    framework.
        NewSuite("my_test", m).
        Run()
}</pre>
<p>在此函数中你需要调用NewSuite，从而：</p>
<ol>
<li>启动一个平台特定的环境，默认使用本地环境，如果需要在K8S上运行测试，设置标记<span style="color: #24292e;"><pre class="crayon-plain-tag">--istio.test.env=kube</pre></span></li>
<li>运行当前包的所有测试用例</li>
<li>停止环境</li>
</ol>
<p>然后在当前包中编写一个个的测试用例：</p>
<pre class="crayon-plain-tag">func TestHTTP(t *testing.T) {
    // 获取测试环境上下文
    ctx := framework.GetContext(t)
    defer ctx.Done()
    
    // 获取需要测试的组件（例如Pilot、Mixer、Apps）
    apps := components.GetApps(t, ctx)
    a := apps.GetAppOrFail("a", t)
    b := apps.GetAppOrFail("b", t)

    // 和组件进行交互，每个组件都定义了自己的API
    be := b.EndpointsForProtocol(model.ProtocolHTTP)[0]
    result := a.CallOrFail(be, components.AppCallOptions{}, t)[0]
    if !result.IsOK() {
        t.Fatalf("HTTP Request unsuccessful: %s", result.Body)
    }
}</pre>
<p>如果你需要执行测试套件级别的检查，可以：</p>
<pre class="crayon-plain-tag">func TestMain(m *testing.M) {
    framework.NewTest("my_test", m).
    // 要求Kubernetes环境
    RequireEnvironment(environment.Kube).                             
    // 部署供整个测试套件使用的Istio 
    SetupOnEnv(environment.Kube, istio.Setup(&amp;ist, setupIstioConfig)).
    // 调用你的setp函数
    Setup(setup).
    Run()
}

func setupIstioConfig(cfg *istio.Config) {
    cfg.Values["your-feature-enabled"] = "true"
}

func setup(ctx resource.Context) error {
  // 准备测试环境
}</pre>
<div class="blog_h3"><span class="graybg">支持的环境</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 120px;">Native</td>
<td>
<p>在本机（进程内或进程外）运行测试，默认值</p>
<p>好处是简单、快、可靠</p>
</td>
</tr>
<tr>
<td>Kubernetes</td>
<td>需要使用标记<pre class="crayon-plain-tag">--istio.test.env=kube</pre>，默认情况下会使用<pre class="crayon-plain-tag">~/.kube/config</pre>来部署Istio</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">标记</td>
<td style="width: 20%; text-align: center;">默认值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>istio.test.env</td>
<td>native</td>
<td>
<p>运行测试的环境</p>
</td>
</tr>
<tr>
<td>istio.test.work_dir</td>
<td>''</td>
<td>创建 logs/temp文件的本地目录，如果不指定使用系统临时目录</td>
</tr>
<tr>
<td>istio.test.hub</td>
<td>''</td>
<td>使用的Docker仓库，默认从HUB环境变量读取</td>
</tr>
<tr>
<td>istio.test.tag</td>
<td>''</td>
<td>使用的镜像标签，默认从TAG环境变量读取</td>
</tr>
<tr>
<td>istio.test.pullpolicy</td>
<td>Always</td>
<td>镜像拉取策略，可用环境变量PULL_POLICY指定</td>
</tr>
<tr>
<td>istio.test.nocleanup</td>
<td>false</td>
<td>测试完毕后不要清理资源</td>
</tr>
<tr>
<td>istio.test.ci</td>
<td>false</td>
<td>启用CI模式，以打印更多日志和状态信息</td>
</tr>
<tr>
<td>istio.test.kube.config</td>
<td>~/.kube/config</td>
<td>使用的Kubeconfig</td>
</tr>
<tr>
<td>istio.test.kube.minikube</td>
<td>false</td>
<td>基于Minikube环境运行</td>
</tr>
<tr>
<td>istio.test.kube.systemNamespace</td>
<td>istio-system</td>
<td>废弃</td>
</tr>
<tr>
<td>istio.test.kube.istioNamespace</td>
<td>istio-system</td>
<td>Istio CA和证书分发组件所在命名空间</td>
</tr>
<tr>
<td>istio.test.kube.configNamespace</td>
<td>istio-system</td>
<td>配置文件、服务发现、自动注入组件部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.telemetryNamespace</td>
<td>istio-system</td>
<td>mixer, kiali, tracing providers, graphana, prometheus 部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.policyNamespace</td>
<td>istio-system</td>
<td>policy checker部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.ingressNamespace</td>
<td>istio-system</td>
<td>ingressgateway部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.egressNamespace</td>
<td>istio-system</td>
<td>egressgateway部署到的命名空间</td>
</tr>
<tr>
<td>istio.test.kube.deploy</td>
<td>true</td>
<td>如果为true则部署组件，否则假设组件已经部署了</td>
</tr>
<tr>
<td>istio.test.kube.helm.chartDir</td>
<td>$(ISTIO)/install/kubernetes/helm/istio</td>
<td>Istio的Helm Chart位置</td>
</tr>
<tr>
<td>istio.test.kube.helm.valuesFile</td>
<td>values-e2e.yaml</td>
<td>相对于relative to istio.test.kube.helm.chartDir的Chart 覆盖变量文件</td>
</tr>
<tr>
<td>istio.test.kube.helm.values</td>
<td>''</td>
<td>提供Chart覆盖变量</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">使用Prow</span></div>
<p><a href="https://prow.istio.io/">Prow</a>提供CI特性、一套工具集来提升开发人员额度生产力，它由K8S社区开发，部署在GCE中。你也可以在任何K8S集群中部署它。</p>
<p>Prow能运行：<span style="background-color: #c0c0c0;">pre-submit、post-submit、周期性的Jobs</span>，并提供生产力工具：</p>
<ol>
<li>Tide：自动合并PR</li>
<li>hold：保持没有被合并的PR</li>
<li>分支保护：基于配置更新Github分支保护策略</li>
<li>needs-rebase：提示PR需要rebase</li>
</ol>
<div class="blog_h3"><span class="graybg">配置</span></div>
<p>配置文件主要有两个：</p>
<ol>
<li>config.yaml：定义Job、一般性设置</li>
<li>plugins.yaml：插件配置</li>
</ol>
<div class="blog_h1"><span class="graybg">开发模板</span></div>
<p>&nbsp;</p>
<p>Mixer使用模板（Template）来结构化入站的属性。模板描述了需要发送给适配器的<span style="background-color: #c0c0c0;">数据的形式</span>，它还定义了适配器为了接受数据<span style="background-color: #c0c0c0;">必须实现的gRPC接口</span>。Mixer提供了一些开箱即用的默认模板，当<span style="background-color: #c0c0c0;">实现自己的适配器时，应当尽可能使用这些默认模板</span>。</p>
<p>模板以Proto文件的形式声明，此定义中包含Template消息，指定了Template变体（Check/Report/Quota）。从Template消息会生成：</p>
<ol>
<li>InstanceMsg消息，在请求期间，作为参数传递</li>
<li>Handle服务，InstanceMsg消息传递给该服务</li>
<li>Type消息，在配置期间传递，描述InstanceMsg的规格</li>
</ol>
<p>如果可能，不要自己定义模板。Istio内置的模板通常可以满足需要。</p>
<div class="blog_h2"><span class="graybg">Proto文件</span></div>
<p>前面提到过，Template是使用Proto文件定义的，它对应一个名为<span style="color: #24292e;">Template的消息。所有Go代码都基于此消息自动生成。</span></p>
<p>每个模板具有两个额外的属性：</p>
<ol>
<li>Name，模板的独特的名称。<span style="background-color: #c0c0c0;">适配器会使用此名称来注册到Mixer，声明自己需要消费这种类型模板的Instance</span></li>
<li>template_variety，表示模板的种类，种类<span style="background-color: #c0c0c0;">决定了</span>适配器必须实现的、消费模板Instance的<span style="background-color: #c0c0c0;">方法的签名</span>
<ol>
<li>Check，这种模板需要的实例仅仅在Mixer客户端进行Check API调用时生成</li>
<li>Report，这种模板需要的实例仅仅在Mixer客户端进行Report API调用时生成</li>
<li>Quota，这种模板需要的实例仅仅在Mixer客户端进行Check API调用，以要求进行Quota分配时生成</li>
<li>AttributeGenerator，这种模板需要的实例在Check/Report调用时都会生成并分发，这种模板的处理发生在补充属性生成阶段（supplementary attribute generation phase） —— <span style="background-color: #c0c0c0;">早于任何其它模板的处理</span>。处理AttributeGenerator的适配器称为属性生成适配器，它们负责生成模板声明的输出数据，你可以基于这些数据来配置新的属性</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">示例</span></div>
<p>下面是<span style="color: #24292e;">listentry模板的Proto文件：</span></p>
<pre class="crayon-plain-tag">syntax = "proto3";

// 模板的包名词，它决定了模板的名字，对应CRD的名字
package listEntry;

import "mixer/adapter/model/v1beta1/extensions.proto";

// 这是一个CHECK模板，可选的种类 CHECK, REPORT, QUOTA, or ATTRIBUTE_GENERATOR
// 种类决定了在Mixer处理流水线的什么地方调用消费此模板的适配器
option (istio.mixer.adapter.model.v1beta1.template_variety) = TEMPLATE_VARIETY_CHECK;


// 配置示例：
//
// apiVersion: "config.istio.io/v1alpha2"
// kind: listentry
// metadata:
//   name: appversion
//   namespace: istio-system
// spec:
//   实例的数据
//   value: source.labels["version"]

// 根据模板类型的不同，需要定义不同的消息。你总是需要定义一个名为Template的消息
message Template {
    // 实例的元数据，决定了在运行时，此模板的实例是什么形状，实例会发送给适配器进行处理
    string value = 1;
}</pre>
<p>&nbsp;</p>
<p>需要注意：Template消息上面的注释，将用作模板的文档，该文档同时面向适配器开发人员、运维操作人员。 </p>
<div class="blog_h3"><span class="graybg">OutputTemplate</span></div>
<p>对于ATTRIBUTE_GENERATOR类型的模板，还需要定义一个额外的OutputTemplate消息：</p>
<ol>
<li>Template消息，定义传递给使用该模板实例的适配器的输入</li>
<li>OutputTemplate消息，定义上述适配器需要返回的输出</li>
</ol>
<div class="blog_h3"><span class="graybg">字段类型</span></div>
<p>注意：目前不支持内嵌Message，enum，oneof，repeated。</p>
<p>可以在Proto中使用的模板字段类型包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模板字段类型</td>
<td style="width: 30%; text-align: center;">Go字段类型</td>
</tr>
</thead>
<tbody>
<tr>
<td>string</td>
<td>string</td>
</tr>
<tr>
<td>int64</td>
<td>int64</td>
</tr>
<tr>
<td>double</td>
<td>float64</td>
</tr>
<tr>
<td>bool</td>
<td>bool</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.TimeStamp</td>
<td>time.Time</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.Duration</td>
<td>time.Duration</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.IPAddress</td>
<td>net.IP</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.DNSName</td>
<td>adapter.DNSName</td>
</tr>
<tr>
<td>istio.mixer.adapter.model.v1beta1.Value</td>
<td>interface{}</td>
</tr>
<tr>
<td>map&lt;string, string&gt;</td>
<td>map[string]string</td>
</tr>
<tr>
<td>map&lt;string, int64&gt;</td>
<td>map[string]int64</td>
</tr>
<tr>
<td>map&lt;string, double&gt;</td>
<td>map[string]float64</td>
</tr>
<tr>
<td>map&lt;string, bool&gt;</td>
<td>map[string]bool</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.TimeStamp&gt;</td>
<td>map[string]time.Time</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.Duration&gt;</td>
<td>map[string]time.Duration</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.IPAddress&gt;</td>
<td>map[string]net.IP</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.DNSName&gt;</td>
<td>map[string]adapter.DNSName</td>
</tr>
<tr>
<td>map&lt;string, istio.mixer.adapter.model.v1beta1.Value&gt;</td>
<td>map[string]interface{}</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">生成的代码</span></div>
<p>基于上述Proto文件生成的Go代码包括：</p>
<ol>
<li>InstanceMsg结构：定义了在请求期间传递给适配器的数据的结构。Mixer会基于请求属性和你给出的配置，生成此结构的实例</li>
<li>OutputMsg结构：仅AttributeGenerator模板生成此结构。定义在属性生成阶段，适配器应当返回的数据的结构</li>
<li>Handler***Service服务：定义Mixer用来分发InstanceMsg消息给适配器时使用的gRPC接口</li>
<li>Type结构：如果InstanceMsg中的某些字段的数据类型是动态的（istio.policy.v1beta1.Value），则你提供的配置决定这些字段的<a href="https://github.com/istio/api/blob/master/policy/v1beta1/value_type.proto">真实类型</a></li>
</ol>
<p>注意：生成的服务接口，由<span style="background-color: #c0c0c0;">消费模板实例的那些适配器负责实现</span>。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">REPORT模板</span></div>
<p>这是内置的metric模板的例子：</p>
<pre class="crayon-plain-tag">syntax = "proto3";

package metric;

import "mixer/adapter/model/v1beta1/type.proto";
import "mixer/adapter/model/v1beta1/extensions.proto";

option (istio.mixer.v1.config.template.template_variety) = TEMPLATE_VARIETY_REPORT;

// 表示需要报告的单个数据
message Template {
   // 报告的值
   istio.mixer.adapter.model.v1beta1.Value value = 1;

   // 唯一性标识此指标的维度列表
   map&lt;string, istio.mixer.adapter.model.v1beta1.Value&gt; dimensions = 2;
}</pre>
<p>自动生成如下供适配器使用的Proto定义：</p>
<pre class="crayon-plain-tag">// 需要处理请求期间metric类型的实例的适配器，都需要实现该服务接口
service HandleMetricService {
    // 处理指标
    rpc HandleMetric(HandleMetricRequest) returns (istio.mixer.adapter.model.v1beta1.ReportResult);

}

// 请求消息结构
message HandleMetricRequest {

    // metric的实例
    repeated InstanceMsg instances = 1;

    // 适配器特定的Handler配置
    //
    // 注意：可以实现InfrastructureBackend服务，从而可以在会话创建（InfrastructureBackend.CreateSession）期间
    // 接收处理器配置。在这种情况下，adapter_config会包含type_url: google.protobuf.Any.type_url字段，并且包含
    // 由InfrastructureBackend.CreateSession调用返回的session_id: string
    google.protobuf.Any adapter_config = 2;

    // 用于去除针对Mixer的重复调用
    string dedup_id = 3;
}

// metric模板的载荷
message InstanceMsg {

    // 实例名
    string name = 72295727;

    // 报告的值
    istio.policy.v1beta1.Value value = 1;

    // 指标的维度
    map&lt;string, istio.policy.v1beta1.Value&gt; dimensions = 2;
}

// 包含推断出的、metric模板实例的类型信息
// 在配置期间，通过InfrastructureBackend.CreateSession调用传递
message Type {

    // The value being reported.
    istio.policy.v1beta1.ValueType value = 1;

    // The unique identity of the particular metric to report.
    map&lt;string, istio.policy.v1beta1.ValueType&gt; dimensions = 2;
}</pre>
<div class="blog_h3"><span class="graybg">CHECK模板</span></div>
<p>这是内置listentry模板的例子： </p>
<pre class="crayon-plain-tag">syntax = "proto3";

package listentry;

import "mixer/adapter/model/v1beta1/extensions.proto";

option (istio.mixer.v1.config.template.template_variety) = TEMPLATE_VARIETY_CHECK;

message Template {
    string value = 1;
}</pre>
<p>自动生成如下供适配器使用的Proto定义：</p>
<pre class="crayon-plain-tag">service HandleListEntryService {
    rpc HandleListEntry(HandleListEntryRequest) returns (istio.mixer.adapter.model.v1beta1.CheckResult);
}

message HandleListEntryRequest {
    InstanceMsg instance = 1;
    google.protobuf.Any adapter_config = 2;
    string dedup_id = 3;
}

message InstanceMsg {
    string name = 72295727;
    string value = 1;
}

message Type {
}</pre>
<div class="blog_h3"><span class="graybg">QUOTA模板</span></div>
<pre class="crayon-plain-tag">package quota;

import "policy/v1beta1/type.proto";
import "mixer/adapter/model/v1beta1/extensions.proto";

option (istio.mixer.adapter.model.v1beta1.template_variety) = TEMPLATE_VARIETY_QUOTA;

message Template {
    map&lt;string, istio.policy.v1beta1.Value&gt; dimensions = 1;
}</pre>
<p>自动生成如下供适配器使用的Proto定义：  </p>
<pre class="crayon-plain-tag">service HandleQuotaService {
    rpc HandleQuota(HandleQuotaRequest) returns (istio.mixer.adapter.model.v1beta1.QuotaResult);

}

message HandleQuotaRequest {

    InstanceMsg instance = 1;
    google.protobuf.Any adapter_config = 2;
    string dedup_id = 3;
    istio.mixer.adapter.model.v1beta1.QuotaRequest quota_request = 4;
}

message InstanceMsg {
    string name = 72295727;
    map&lt;string, istio.policy.v1beta1.Value&gt; dimensions = 1;

}

message Type {
    map&lt;string, istio.policy.v1beta1.ValueType&gt; dimensions = 1;
} </pre>
<div class="blog_h1"><span class="graybg">开发适配器</span></div>
<p>注意：早先Istio支持扩展进程内的适配器，这种适配器（和内置适配器一样）是在Mixer进程内部运行的。目前进程内适配器已经被弃用，应该考虑开发进程外（Out Of Process）的gRPC适配器。</p>
<p>适配器将Mixer和各种基础设施后端，例如负责指标采集的Prometheus、负责日志收集的Fluentd，集成起来。Mixer是一个属性处理引擎，它负责基于用户提供的配置来将请求属性映射为适配器输入参数，然后通过适配器来调用后端系统。</p>
<div class="blog_h2"><span class="graybg">两种实现方式</span></div>
<p>gRPC适配器可以由两种实现模型——基于会话或者无会话的。</p>
<div class="blog_h3"><span class="graybg">基于会话</span></div>
<p>注意：目前尚未支持。</p>
<p>Mixer仅仅在使用会话标识符创建会话时，将适配器的配置信息传递给适配器一次。未来Mixer和适配器的通信，均是基于会话标识符，你需要通过此标识符来引用最初传入的配置。</p>
<p>基于这种实现模型的适配器，需要在实现Handle***服务的同时实现<a href="https://github.com/istio/api/blob/master/mixer/adapter/model/v1beta1/infrastructure_backend.proto">InfrastructureBackend</a>服务，Mixer调用后者方法的时序如下：</p>
<ol>
<li>调用Validate方法</li>
<li>调用CreateSession方法，返回的session_id将用作后续的
<ol>
<li>针对Handle***的实时调用</li>
<li>最终的CloseSession调用</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">无会话</span></div>
<p>适配器仅仅需要实现Handle***服务，Mixer仅仅在请求（Check/Report/Quota）时期和适配器交互，每次交互都传递完整的适配器配置信息。</p>
<div class="blog_h2"><span class="graybg">添加到Mixer</span></div>
<p>每种适配器都需要提供一个资源配置，你需要在Istio的配置存储中添加该配置。<span style="background-color: #c0c0c0;">资源配置是adapter类型的CR</span>。</p>
<p>创建这种资源配置的方法有两种：</p>
<ol>
<li>调用工具<pre class="crayon-plain-tag">mixer/tool/mixgen</pre>，创建一个adapter资源：<br />
<pre class="crayon-plain-tag">//go:generate go run $GOPATH/src/istio.io/istio/mixer/tools/mixgen/main.go adapter \
  # 适配器类型    是否基于会话  消费的模板类型
  -n mygrpcadapter -s=false -t metric  \
  -c $GOPATH/src/istio.io/istio/mixer/adapter/mygrpcadapter/config/config.proto_descriptor \
  -o mygrpcadapter-nosession.yaml</pre>
</li>
<li>
<p>使用<pre class="crayon-plain-tag">mixer_codegen.sh -a</pre> 命令传入适配器的config.proto：</p>
<p><pre class="crayon-plain-tag">//go:generate $GOPATH/src/istio.io/istio/bin/mixer_codegen.sh \
  -a mixer/adapter/mygrpcadapter/config/config.proto -x "-n mygrpcadapter -s=false -t metric "</pre>
</li>
</ol>
<p>不管使用哪种方式，命令都可以作为<pre class="crayon-plain-tag">go generate</pre>阶段的一部分自动执行，都会生成adapter类型的CR。对于上面的例子，会生成一个无会话的、支持metric模板的，名为mygrpcadapter的资源配置，适配器的配置的Proto（声明该适配器支持哪些配置项）也会被编码到其中：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: adapter
metadata:
  name: mygrpcadapter
  namespace: istio-system
spec:
  description:
  # 是否基于会话
  session_based: false
  # 支持的模板
  templates:
  - metric
  # 适配器配置的Proto
  config: CsD3AgogZ29vZ2xlL3Byb3RvYnVmL2Rlc2NyaXB0b3.....</pre>
<div class="blog_h2"><span class="graybg">测试适配器 </span></div>
<p>Istio提供了一个简单的用于测试适配器的框架。该框架会：</p>
<ol>
<li>创建一个进程内的Mixer gRPC服务器，该服务器使用基于本地文件系统的配置存储。</li>
<li>创建一个Mixer gRPC客户端</li>
</ol>
<p>测试框架的实现位于pkg/adapter/test/integration.go。Istio提供了基于此测试框架来<a href="https://github.com/istio/istio/blob/master/mixer/test/prometheus/prometheus_integration_test.go">测试Prometheus REPORT适配器</a>的例子。</p>
<div class="blog_h2"><span class="graybg">生成CRD</span></div>
<p>Mixs支持为自定义的适配器生成专门的CRD，执行下面的命令：</p>
<pre class="crayon-plain-tag">$GOPATH/out/linux_amd64/release/mixs crd adapter</pre>
<p>此Mixer内嵌的适配器的CRD信息会打印到控制台。找到自定义适配器的CRD，用kubectl命令存储到K8S中即可。</p>
<p>有了专门的CRD后，你不需要使用通用的handler来创建处理器，直接创建CR即可。</p>
<div class="blog_h1"><span class="graybg">适配器示例</span></div>
<div class="blog_h2"><span class="graybg">REPORT适配器</span></div>
<p>本节给出实现、测试、插入一个简单的进程外gRPC适配器的完整例子。该适配器：</p>
<ol>
<li>支持metric模板</li>
<li>对于每个请求，打印它从Mixer接收的数据到文件</li>
</ol>
<div class="blog_h3"><span class="graybg">准备</span></div>
<p>在开始前，请参考“如何贡献”一节，签出Istio代码，准备好环境：</p>
<ol>
<li>你需要安装<a href="https://github.com/protocolbuffers/protobuf/releases">3.5.1</a>或者更高版本的<span style="color: #24292e;">protoc（Protocol编译器）</span></li>
<li>设置环境变量：<br />
<pre class="crayon-plain-tag">export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer
export ISTIO=$GOPATH/src/istio.io </pre>
</li>
<li>确保Mixer能构建成功：<br />
<pre class="crayon-plain-tag">pushd $ISTIO/istio &amp;&amp; make mixs</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">编写骨架代码</span></div>
<p>在Istio源码树中为新的适配器创建目录：</p>
<pre class="crayon-plain-tag">cd $MIXER_REPO/adapter &amp;&amp; mkdir mygrpcadapter &amp;&amp; cd mygrpcadapter</pre>
<p>然后在mygrpcadapter.go中编写如下骨架代码：</p>
<pre class="crayon-plain-tag">package mygrpcadapter

import (
	"context"
	"fmt"
	"net"

	"google.golang.org/grpc"

	"istio.io/api/mixer/adapter/model/v1beta1"
	"istio.io/istio/mixer/template/metric"
)

type (
	// gRPC服务器的接口
	Server interface {
		Addr() string
		Close() error
		Run(shutdown chan error)
	}

	// 适配器结构
	MyGrpcAdapter struct {
		listener net.Listener
		server   *grpc.Server
	}
)

// 该适配器消费metric实例，因此必须实现下面的接口
var _ metric.HandleMetricServiceServer = &amp;MyGrpcAdapter{}

// 编写所有接口方法的骨架

/* 实现HandleMetricServiceServer */
func (s *MyGrpcAdapter) HandleMetric(ctx context.Context, r *metric.HandleMetricRequest) (*v1beta1.ReportResult, error) {
	return nil, nil
}


/* 实现Server */
func (s *MyGrpcAdapter) Addr() string {
	return s.listener.Addr().String()
}
func (s *MyGrpcAdapter) Run(shutdown chan error) {
        // 传递监听器，启动gRPC服务器
	shutdown&lt;- s.server.Serve(s.listener)
}

// 优雅关闭服务器，测试用
func (s *MyGrpcAdapter) Close() error {
	if s.server != nil {
		s.server.GracefulStop()
	}

	if s.listener != nil {
		_ = s.listener.Close()
	}

	return nil
}

// 创建gRPC服务器并监听
func NewMyGrpcAdapter(addr string) (Server, error) {
	if addr == "" {
		addr = "0"
	}
	listener, err := net.Listen("tcp", fmt.Sprintf(":%s", addr))
	if err != nil {
		return nil, fmt.Errorf("unable to listen on socket: %v", err)
	}
	s := &amp;MyGrpcAdapter{
		listener: listener,
	}
	fmt.Printf("listening on \"%v\"\n", s.Addr())
	s.server = grpc.NewServer()
	metric.RegisterHandleMetricServiceServer(s.server, s)
	return s, nil
}</pre>
<p>执行下面的命令，确保能构建成功：</p>
<pre class="crayon-plain-tag">go build ./...</pre>
<div class="blog_h3"><span class="graybg">编写适配器配置</span></div>
<p>我们开发的适配器需要将接收到的信息打印到文件中，因此需要一个参数，提供文件的路径。</p>
<p>创建mygrpcadapter/config子目录，然后创建Proto文件config.proto：</p>
<pre class="crayon-plain-tag">syntax = "proto3";

// 包名
package adapter.mygrpcadapter.config;

import "gogoproto/gogo.proto";

// 生成的Go代码使用的包名
option go_package="config";

// 适配器的配置，使用Params消息表示
message Params {
    // 文件路径
    string file_path = 1;
}</pre>
<p>我们需要从上述Proto生成对应的Go源码，以及适配器的adaptor CR。 在适配器源码上添加以下注释：</p>
<pre class="crayon-plain-tag">// nolint:lll
//go:generate $GOPATH/src/istio.io/istio/bin/mixer_codegen.sh -a mixer/adapter/mygrpcadapter/config/config.proto -x "-s=false -n mygrpcadapter -t metric"

package mygrpcadapter</pre>
<p>并执行下面的命令：</p>
<pre class="crayon-plain-tag">go generate ./...
go build ./...</pre>
<p>如果一切正常，会生成以下文件：</p>
<ol>
<li>类型为adapter的自定义资源，<span style="background-color: #c0c0c0;">此资源提供自定义适配器的元数据，包括是否基于会话、描述、适配器参数信息</span>：<br />
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: adapter
metadata:
  name: mygrpcadapter
  namespace: istio-system
spec:
  description:
  session_based: false
  templates:
  - metric
  Config: ... </pre>
</li>
<li>Config.pb.go，适配器的配置的Go代码</li>
<li>mysampleadapter.config.pb.html，适配器的文档</li>
<li>Config.proto_descriptor，一个中介文件，适配器代码不会直接使用它</li>
</ol>
<div class="blog_h3"><span class="graybg">完善业务逻辑</span></div>
<p>适配器完整的代码如下： </p>
<pre class="crayon-plain-tag">// nolint:lll
// Generates the mygrpcadapter adapter's resource yaml. It contains the adapter's configuration, name, supported template
// names (metric in this case), and whether it is session or no-session based.
//go:generate $GOPATH/src/istio.io/istio/bin/mixer_codegen.sh -a mixer/adapter/mygrpcadapter/config/config.proto -x "-s=false -n mygrpcadapter -t metric"

package mygrpcadapter

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"bytes"
	"os"

	"istio.io/api/mixer/adapter/model/v1beta1"
	policy "istio.io/api/policy/v1beta1"
	"istio.io/istio/mixer/adapter/mygrpcadapter/config"
	"istio.io/istio/mixer/template/metric"
	"istio.io/istio/pkg/log"
)

type (
	Server interface {
		Addr() string
		Close() error
		Run(shutdown chan error)
	}

	MyGrpcAdapter struct {
		listener net.Listener
		server   *grpc.Server
	}
)

var _ metric.HandleMetricServiceServer = &amp;MyGrpcAdapter{}

func (s *MyGrpcAdapter) HandleMetric(ctx context.Context, r *metric.HandleMetricRequest) (*v1beta1.ReportResult, error) {

	log.Infof("received request %v\n", *r)
	var b bytes.Buffer
        // 配置参数
	cfg := &amp;config.Params{}

	if r.AdapterConfig != nil {
                //  将请求中附带的适配器配置进行反序列化处理
		if err := cfg.Unmarshal(r.AdapterConfig.Value); err != nil {
			log.Errorf("error unmarshalling adapter config: %v", err)
			return nil, err
		}
	}

	b.WriteString(fmt.Sprintf("HandleMetric invoked with:\n  Adapter config: %s\n  Instances: %s\n", cfg.String(), instances(r.Instances)))
	if cfg.FilePath == "" {
		fmt.Println(b.String())
	} else {
               // 输出到文件
		_, err := os.OpenFile("out.txt", os.O_RDONLY|os.O_CREATE, 0666)
		f, err := os.OpenFile(cfg.FilePath, os.O_APPEND|os.O_WRONLY, 0600)
		defer f.Close()
		log.Infof("writing instances to file %s", f.Name())
	}
        // 返回空的报告结果
	return &amp;v1beta1.ReportResult{}, nil
}

// 解码metric的维度，注意维度值类型可以是动态的
func decodeDimensions(in map[string]*policy.Value) map[string]interface{} {
	out := make(map[string]interface{}, len(in))
	for k, v := range in {
		out[k] = decodeValue(v.GetValue())
	}
	return out
}

// 解码metric的值，注意值的类型可以是动态的
func decodeValue(in interface{}) interface{} {
	switch t := in.(type) {
	case *policy.Value_StringValue:
		return t.StringValue
	case *policy.Value_Int64Value:
		return t.Int64Value
	case *policy.Value_DoubleValue:
		return t.DoubleValue
	default:
		return fmt.Sprintf("%v", in)
	}
}

func instances(in []*metric.InstanceMsg) string {
	var b bytes.Buffer
        // 对于每个InstanceMsg，解码其值、维度，并打印
	for _, inst := range in {
		b.WriteString(fmt.Sprintf("'%s':\n"+
			"  {\n"+
			"		Value = %v\n"+
			"		Dimensions = %v\n"+
			"  }", inst.Name, decodeValue(inst.Value.GetValue()), decodeDimensions(inst.Dimensions)))
	}
	return b.String()
}

// ...</pre>
<p>编写一个main函数，以独立进程的形式启动该适配器：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"os"

	mygrpcadapter "istio.io/istio/mixer/adapter/mygrpcadapter"
)

func main() {
	addr := ""
	if len(os.Args) &gt; 1 {
		addr = os.Args[1]
	}
	s, err := mygrpcadapter.NewMyGrpcAdapter(addr)
	shutdown := make(chan error, 1)
	go func() {
		s.Run(shutdown)
	}()
	_ = &lt;-shutdown
}</pre>
<div class="blog_h3"><span class="graybg">编写Istio配置</span></div>
<p>要使用上述适配器，你需要配置三类Istio资源：</p>
<ol>
<li>处理器（Handler）：为适配器提供配置参数</li>
<li>实例（Instance）：指定如何从请求属性来生成实例，在这里就是metric</li>
<li>规则（Rule） ：将处理器和实例组合起来</li>
</ol>
<p>配置示例如下：</p>
<pre class="crayon-plain-tag"># 处理器配置
apiVersion: "config.istio.io/v1alpha2"
# 那些基于内置适配器的处理器，类型可以是prometheus, fluentd ... 
# 基于自定义适配器的，可以统一配置为handler
kind: handler
metadata:
 name: h1
 namespace: istio-system
spec:
 # 需要指定适配器类型
 adapter: mygrpcadapter
 connection:
   # address: "{ADDRESS}"
   address: "127.0.0.1：38355"
 # 适配器参数
 params:
   file_path: "out.txt"
---

# 模板metric的实例
apiVersion: "config.istio.io/v1alpha2"
kind: instance
metadata:
 name: i1metric
 namespace: istio-system
spec:
 template: metric
 params:
   value: request.size | 0
   dimensions:
     response_code: "200"
---

# 规则
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
 name: r1
 namespace: istio-system
spec:
 actions:
 - handler: h1.istio-system
   instances:
   - i1metric
---</pre>
<div class="blog_h3"><span class="graybg">启动Mixer并验证适配器 </span></div>
<p>首先启动适配器，注意我们没有指定端口，随机分配的监听端口会打印到标准输出：</p>
<pre class="crayon-plain-tag">export ISTIO=$GOPATH/src/istio.io
export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer
cd $MIXER_REPO/adapter/mygrpcadapter
go run cmd/main.go 127.0.0.1：38355</pre>
<p>我们使用文件系统作为Mixer的配置存储， 将所有配置文件拷贝到同一目录：</p>
<pre class="crayon-plain-tag">mkdir testdata
# 处理器、实例、规则
cp sample_operator_cfg.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
# 适配器CR
cp config/mygrpcadapter.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
# Istio供测试使用的属性清单
cp $MIXER_REPO/testdata/config/attributes.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
# Metric模板
cp $MIXER_REPO/template/metric/template.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata</pre>
<p>构建Mixer，并从上述配置存储启动它：</p>
<pre class="crayon-plain-tag">pushd $ISTIO/istio &amp;&amp; make mixs
$GOPATH/out/linux_amd64/release/mixs server --configStoreURL=fs://$(pwd)/mixer/adapter/mygrpcadapter/testdata</pre>
<p>启动Mixer后，可以用命令行工具mixc来向Mixer报告：</p>
<pre class="crayon-plain-tag">pushd $ISTIO/istio &amp;&amp; make mixc
                                            # 报告字符串属性                            报告整数属性
$GOPATH/out/linux_amd64/release/mixc report -s destination.service="svc.cluster.local" -i request.size=1235</pre>
<p>打开输出文件，应该可以看到如下内容：</p>
<pre class="crayon-plain-tag">HandleMetric invoked with:
  Adapter config: &amp;Params{FilePath:out.txt,}
  Instances: 'i1metric.instance.istio-system':
  {
		Value = 1235
		Dimensions = map[response_code:200]
  }</pre>
<div class="blog_h3"><span class="graybg">编写测试 </span></div>
<p>你可以利用pkg/adapter/test包编写集成测试，启动进程内的Mixer服务器，并通过Mixer客户端调用它：</p>
<pre class="crayon-plain-tag">package mygrpcadapter

import (
  "fmt"
  "io/ioutil"
  "testing"

  adapter_integration "istio.io/istio/mixer/pkg/adapter/test"
  "os"
  "strings"
)

func TestReport(t *testing.T) {
  // 读取适配器的CR
  adptCrBytes, err := ioutil.ReadFile("config/mygrpcadapter.yaml")
  if err != nil {
     t.Fatalf("could not read file: %v", err)
  }
  // 读取处理器、实例、规则配置
  operatorCfgBytes, err := ioutil.ReadFile("sample_operator_cfg.yaml")
  if err != nil {
     t.Fatalf("could not read file: %v", err)
  }
  operatorCfg := string(operatorCfgBytes)
  shutdown := make(chan error, 1)

  // 输出文件
  var outFile *os.File
  outFile, err = os.OpenFile("out.txt", os.O_RDONLY|os.O_CREATE, 0666)
  if err != nil {
     t.Fatal(err)
  }
  defer func() {
     // 测试完毕后移除输出文件
     if removeErr := os.Remove(outFile.Name()); removeErr != nil {
        t.Logf("Could not remove temporary file %s: %v", outFile.Name(), removeErr)
     }
  }()

  // 适配器集成测试框架
  adapter_integration.RunTest(
     t,
     nil,
     // Scenario定义一个完整的集成测试场景
     adapter_integration.Scenario{

        // 测试前的准备
        Setup: func() (ctx interface{}, err error) {
           // 创建适配器
           pServer, err := NewMyGrpcAdapter("")
           if err != nil {
              return nil, err
           }
           go func() {
              // 启动服务器
              pServer.Run(shutdown)
              _ = &lt;-shutdown
           }()
           return pServer, nil
        },
        // 测试后清理
        Teardown: func(ctx interface{}) {
           s := ctx.(Server)
           s.Close()
        },
        // 需要对Mixer并行发起的调用列表
        ParallelCalls: []adapter_integration.Call{
           {
              CallKind: adapter_integration.REPORT,
              Attrs:    map[string]interface{}{"request.size": int64(555)},
           },
        },
        // 测试结果验证
        GetState: func(ctx interface{}) (interface{}, error) {
           bytes, err := ioutil.ReadFile("out.txt")
           if err != nil {
              return nil, err
           }
           s := string(bytes)
           wantStr := `HandleMetric invoked with:
               Adapter config: &amp;Params{FilePath:out.txt,}
               Instances: 'i1metric.instance.istio-system':
               {
                   Value = 555
                   Dimensions = map[response_code:200]
               }
           `
           // 断言失败
           if normalize(s) != normalize(wantStr) {
              return nil, fmt.Errorf("got adapters state as : '%s'; want '%s'", s, wantStr)
           }
           return nil, nil
        },
        // Mixer需要读取的CRDs数组
        GetConfig: func(ctx interface{}) ([]string, error) {
           s := ctx.(Server)
           return []string{
              string(adptCrBytes),
              strings.Replace(operatorCfg, "{ADDRESS}", s.Addr(), 1),
           }, nil
        },

        // 期望的测试结果的JSON字符串形式
        Want: `
            {
             "AdapterState": null,
             "Returns": [
              {
               "Check": {
                "Status": {},
                "ValidDuration": 0,
                "ValidUseCount": 0
               },
               "Quota": null,
               "Error": null
              }
             ]
            }
        `,
     },
  )
}</pre>
<p>执行测试：<pre class="crayon-plain-tag">cd $MIXER_REPO/adapter/mygrpcadapter &amp;&amp; go build ./... &amp;&amp; go test *.go</pre></p>
<div class="blog_h3"><span class="graybg">通信加密</span></div>
<p>Istio支持基于mTLS来保护任何工作负载之间的通信，mTLS同样可以用于进程外适配器和Mixer之间的流量。</p>
<p>任何处理器都可以指定基于mTLS进行双向认证：</p>
<pre class="crayon-plain-tag">apiVersion: "config.istio.io/v1alpha2"
kind: handler
metadata:
 name: h1
 namespace: istio-system
spec:
 adapter: mygrpcadapter
 connection:
  address: "{ADDRESS}" #replaces at runtime by the test
  authentication:
    # 这些数字证书文件必须位于Mixer服务器对应目录
    mutual:
      private_key: "/tmp/grpc-test-key-cert/mixer.key"
      client_certificate: "/tmp/grpc-test-key-cert/mixer.crt"
      ca_certificates: "/tmp/grpc-test-key-cert/ca.pem"</pre>
<p> 改造我们的适配器，使其支持TLS：</p>
<pre class="crayon-plain-tag">// 创建适配器的TLS选项
func getServerTLSOption(credential, privateKey, caCertificate string) (grpc.ServerOption, error) {
        // 从文件加载X509密钥对
	certificate, err := tls.LoadX509KeyPair(
		credential,
		privateKey,
	)
        // 证书池
	certPool := x509.NewCertPool()
	bs, err := ioutil.ReadFile(caCertificate)
        // 将CA证书加入证书池
	ok := certPool.AppendCertsFromPEM(bs)

        // TLS配置
	tlsConfig := &amp;tls.Config{
		Certificates: []tls.Certificate{certificate},
		ClientCAs:    certPool,
	}
        // 要求客户端（Mixer）提供证书，并基于CA验证证书的合法性
	tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
        // 返回ServerOption
	return grpc.Creds(credentials.NewTLS(tlsConfig)), nil
}

func NewMyGrpcAdapter(addr string) (Server, error) {
	if addr == "" {
		addr = "0"
	}
	listener, err := net.Listen("tcp", fmt.Sprintf(":%s", addr))
	if err != nil {
		return nil, fmt.Errorf("unable to listen on socket: %v", err)
	}
	s := &amp;MyGrpcAdapter{
		listener: listener,
	}
	fmt.Printf("listening on \"%v\"\n", s.Addr())

        // 适配器使用的证书
	credential := os.Getenv("GRPC_ADAPTER_CREDENTIAL")
	privateKey := os.Getenv("GRPC_ADAPTER_PRIVATE_KEY")
	certificate := os.Getenv("GRPC_ADAPTER_CERTIFICATE")
	if credential != "" {
                // 获取TLS选项
		so, err := getServerTLSOption(credential, privateKey, certificate)
                // 使用该选项创建gRPC服务器
		s.server = grpc.NewServer(so)
	} else {
		s.server = grpc.NewServer()
	}
	metric.RegisterHandleMetricServiceServer(s.server, s)
	return s, nil
}</pre>
<div class="blog_h2"><span class="graybg">属性生成适配器</span></div>
<p>从Istio 1.1开始支持进程外的属性生成适配器，这类适配器需要实现某种<span style="color: #24292e;">ATTRIBUTE_GENERATOR类型的模板（所生成的接口）。这类适配器的目的是在执行Check/Report调用之前，添加额外的属性。</span></p>
<p>本节，我们会实现一个名为<span style="color: #24292e;">mapper的简单属性生成适配器，它提供一个额外的属性值。</span></p>
<div class="blog_h3"><span class="graybg">定义模板</span></div>
<p>注意：如果Istio内置的模板能满足需要，不要定义自己的模板。在K8S环境下，属性生成器kubernetesenv开箱即用，可以抽取工作负载的各种元数据。</p>
<p>首先在Istio源码树中为我们的适配器创建一个目录：</p>
<pre class="crayon-plain-tag">mkdir -p $GOPATH/src/istio.io/ &amp;&amp; \
cd $GOPATH/src/istio.io/  &amp;&amp; \
git clone https://github.com/istio/istio
cd istio

mkdir -p mixer/adapter/mapper</pre>
<p>然后，定义如下的模板：</p>
<pre class="crayon-plain-tag">syntax = "proto3";
package mapper;
import "mixer/adapter/model/v1beta1/extensions.proto";
option (istio.mixer.adapter.model.v1beta1.template_variety) = TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR;
message Template {
  string key = 1;
}
message OutputTemplate {
  string value = 1;
}</pre>
<p>执行下面的命令，从该模板生成相关文件：</p>
<pre class="crayon-plain-tag">bin/mixer_codegen.sh -t mixer/adapter/mapper/template.proto</pre>
<div class="blog_h3"><span class="graybg">实现适配器 </span></div>
<pre class="crayon-plain-tag">package mapper

import context "golang.org/x/net/context"

type MyAdapter struct{}

// 模板暴露的方法，处理输入模板规定的消息，返回输出模板规定的消息
func (MyAdapter) HandleMapper(_ context.Context, req *HandleMapperRequest) (*OutputMsg, error) {
        lookup := map[string]string{
                "hello": "world",
        }
        // Instance.Key，对应上面模板的Template消息的key字段，注意自动大写
        return &amp;OutputMsg{Value: lookup[req.Instance.Key]}, nil
        // 返回值对存储到OutputTmmplate.value
}</pre>
<div class="blog_h3"><span class="graybg">主函数</span></div>
<pre class="crayon-plain-tag">package main

import (
        "net"
        "google.golang.org/grpc"
        "istio.io/istio/mixer/adapter/mapper"
)

func main() {
        listener, err := net.Listen("tcp", ":38355")
        server := grpc.NewServer()
        // 注册服务实现到gRPC服务器
        mapper.RegisterHandleMapperServiceServer(server, mapper.MyAdapter{})
        server.Serve(listener)
}</pre>
<div class="blog_h3"><span class="graybg">配置适配器 </span></div>
<p>所有适配器都需要提供配置参数，这样Mixer才能调用适配器。对于这个例子，我们只需要一个空的配置参数（Schema）即可：</p>
<pre class="crayon-plain-tag">syntax = "proto3";
package config;
message Params{}</pre>
<p>生成对应的Go代码：</p>
<pre class="crayon-plain-tag">bin/mixer_codegen.sh -a mixer/adapter/mapper/config/config.proto -x "-s=false -n myadapter -t mapper"</pre>
<div class="blog_h3"><span class="graybg">编写Istio配置</span></div>
<pre class="crayon-plain-tag"># template资源
kubectl apply -f mixer/adapter/mapper/template.yaml

# 适配器的adaptor资源
kubectl apply -f mixer/adapter/mapper/config/myadapter.yaml

# 处理器
cat &lt;&lt;EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
  name: h1
  namespace: istio-system
spec:
  adapter: myadapter
  connection:
    address: ":9070"
  params: {}
EOF

# 模板实例
cat &lt;&lt;EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
  name: i1
  namespace: istio-system
spec:
  template: mapper
  params:
    key: destination.namespace
  attribute_bindings:
    source.namespace: output.value | "unknown"
EOF

# 规则
cat &lt;&lt;EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: r1
  namespace: istio-system
spec:
  actions:
  - handler: h1.istio-system
    instances: ["i1"]
EOF</pre>
<div class="blog_h3"><span class="graybg">使用新适配器</span></div>
<p>发起一个报告：</p>
<pre class="crayon-plain-tag">go run mixer/cmd/mixc/main.go report -s destination.namespace="hello"</pre>
<p>查看mixc的调试日志，可以看到在预处理期间生成的source.namespace属性：</p>
<pre class="crayon-plain-tag">debug   api     Dispatching Preprocess
debug   api     Dispatching to main adapters after running preprocessors
debug   api     Attribute Bag: 
destination.namespace         : hello
# 新生成的属性
source.namespace              : world</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/extend-istio">扩展Istio</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/extend-istio/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Istio Mixer与Envoy的交互机制解读</title>
		<link>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy</link>
		<comments>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy#comments</comments>
		<pubDate>Mon, 18 Mar 2019 07:50:34 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[ServiceMesh]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25903</guid>
		<description><![CDATA[<p>前言 在前些日子的文章Istio Pilot与Envoy的交互机制解读中我们详细研究了Istio Pilot如何基于xDS协议和Envoy代理进行各种配置信息的交换。Istio的另一个核心组件是Mixer，它提供三类功能： 遥测报告（Telemetry Reporting），该功能是服务网格可观察性的基础。为服务启用日志记录、监控、追踪、计费流 前置条件检查（Precondition Checking），响应服务请求之前进行一系列检查，例如身份验证、白名单检查、ACL检查 配额管理（Quota Management），基于特定的维度进行配额，控制对受限资源的争用 本文结合源码分析Mixer的设计、实现细节，同时关注Envoy与它的集成机制。 代码结构 istio Mixer的代码位于mixer目录下： 子目录 说明 adapter 包含各种适配器的实现，适配器封装了Mixer和外部基础设施后端（例如Prometheus）交互的逻辑 cmd 包含以下可执行文件的入口点： mixc  用于和Mixer服务器实例进行交互的命令行客户端 <a class="read-more" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">Istio Mixer与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">前言</span></div>
<p>在前些日子的文章<a href="/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>中我们详细研究了Istio Pilot如何基于xDS协议和Envoy代理进行各种配置信息的交换。Istio的另一个核心组件是Mixer，它提供三类功能：</p>
<ol>
<li>遥测报告（Telemetry Reporting），该功能是服务网格可观察性的基础。为服务启用日志记录、监控、追踪、计费流</li>
<li>前置条件检查（Precondition Checking），响应服务请求之前进行一系列检查，例如身份验证、白名单检查、ACL检查</li>
<li>配额管理（Quota Management），基于特定的维度进行配额，控制对受限资源的争用</li>
</ol>
<p>本文结合源码分析Mixer的设计、实现细节，同时关注Envoy与它的集成机制。</p>
<div class="blog_h1"><span class="graybg">代码结构</span></div>
<div class="blog_h2"><span class="graybg">istio</span></div>
<p>Mixer的代码位于<a href="https://github.com/istio/istio/tree/master/mixer">mixer目录</a>下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">子目录</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>adapter</td>
<td>包含各种适配器的实现，适配器封装了Mixer和外部基础设施后端（例如Prometheus）交互的逻辑</td>
</tr>
<tr>
<td>cmd</td>
<td>
<p>包含以下可执行文件的入口点：</p>
<p style="padding-left: 30px;">mixc  用于和Mixer服务器实例进行交互的命令行客户端</p>
<p style="padding-left: 30px;">mixs  在本地启动一个Mixer服务器，或者列出可用的CRD、探测Mixer服务器的状态</p>
</td>
</tr>
<tr>
<td>docker</td>
<td>Docker镜像定义</td>
</tr>
<tr>
<td>template</td>
<td>
<p>模板，Mixer架构的基础构建块，通过自定义模板可以扩展Mixer</p>
<p>模板定义了将请求属性（Attribute）转换为适配器的输入的Schema（类型信息，使用Protubuf语法描述），每个适配器可以支持任意数量的template</p>
<p>模板决定了适配器会收到的数据、也决定了使用适配器必须创建的instance</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Mixs启动过程</span></div>
<p>如果使用Istio官方默认的Chart来部署，则会创建istio-telemetry、istio-policy两套Deployment。它们的启动参数没有区别，分别负责Mixer的遥测、策略检查。这两个Deployment分别对应同名的Service，监听9091端口。</p>
<p>实际上网络监听是由Mixs的Sidecar，也就是Envoy负责的。Mixs Pod本身监听的是UDS  unix:///sock/mixer.socket，Envoy负责将9091端口的请求转发给此UDS。</p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<p>在本地调试Mixer服务端时，参考如下启动参数：</p>
<pre class="crayon-plain-tag">mixs server --port 9091 --monitoringPort 9099  --log_output_level api:debug \
    --configStoreURL=k8s:///home/alex/.kube/config --configDefaultNamespace=istio-system</pre>
<div class="blog_h2"><span class="graybg">入口点</span></div>
<p>mixs server的入口点位于：</p>
<pre class="crayon-plain-tag">func main() {
// supportedTemplates()  map[string]template.Info
// supportedAdapters() []adptr.InfoFn
// 这两个方法都是自动生成的，包含编译的Mixer支持的模板、适配器的列表
// 模板/适配器信息中包含其属性清单
	rootCmd := cmd.GetRootCmd(os.Args[1:], supportedTemplates(), supportedAdapters(), shared.Printf, shared.Fatalf)

	if err := rootCmd.Execute(); err != nil {
		os.Exit(-1)
	}
}

func serverCmd(info map[string]template.Info, adapters []adapter.InfoFn, printf, fatalf shared.FormatFn) *cobra.Command {
// 默认Mixer参数
	sa := server.DefaultArgs()
// 使用自动生成的模板、适配器信息
	sa.Templates = info
	sa.Adapters = adapters

	serverCmd := &amp;cobra.Command{
		Use:   "server",
		Short: "Starts Mixer as a server",
		Run: func(cmd *cobra.Command, args []string) {
// 调用runServer启动服务
			runServer(sa, printf, fatalf)
		},
	}
}

func runServer(sa *server.Args, printf, fatalf shared.FormatFn) {
	// 创建服务器对象
	s, err := server.New(sa)
	// 启动gRPC服务
	s.Run()
	// 等待shutdown信号可读
	err = s.Wait()
        // 执行清理工作
	_ = s.Close()
}</pre>
<div class="blog_h3"><span class="graybg">server.New</span></div>
<p>该函数创建一个全功能的Mixer服务器，并且准备好接收请求：</p>
<pre class="crayon-plain-tag">func New(a *Args) (*Server, error) {
	return newServer(a, newPatchTable())
}</pre>
<div class="blog_h3"><span class="graybg">server.Run</span></div>
<p>该方法启动Mixs服务器：</p>
<pre class="crayon-plain-tag">func (s *Server) Run() {
// 准备好关闭通道
	s.shutdown = make(chan error, 1)
// 设置可用性状态，并通知探针控制器，探针被嵌入到Server
	s.SetAvailable(nil)
	go func() {
		// 启动gRPC服务，传入原始套接字的监听器对象
		err := s.server.Serve(s.listener)

		// 关闭通道
		s.shutdown &lt;- err
	}()
}</pre>
<div class="blog_h3"><span class="graybg">server.Wait</span></div>
<p>该方法很简单，就是在shutdown通道上等待。</p>
<div class="blog_h3"><span class="graybg">server.Close</span></div>
<p>该方法关闭Mixs服务器使用的各种资源。</p>
<div class="blog_h2"><span class="graybg">patchTable</span></div>
<p>newPatchTable创建一个新的patchTable结构：</p>
<pre class="crayon-plain-tag">func newPatchTable() *patchTable {
	return &amp;patchTable{
		newRuntime:    runtime.New,
		configTracing: tracing.Configure,
		startMonitor:  startMonitor,
		listen:        net.Listen,
		configLog:     log.Configure,
		runtimeListen: func(rt *runtime.Runtime) error { return rt.StartListening() },
	}
}</pre>
<p>此结构就是几个函数的集合：</p>
<pre class="crayon-plain-tag">type patchTable struct {
// 此函数创建一个Runtime，Runtime是Mixer运行时环境的主要入口点
// 它监听配置、实例化Handler、创建分发机制（dispatch machinery）、处理请求
	newRuntime func(s store.Store, templates map[string]*template.Info, adapters map[string]*adapter.Info,
		defaultConfigNamespace string, executorPool *pool.GoroutinePool,
		handlerPool *pool.GoroutinePool, enableTracing bool) *runtime.Runtime
// 配置追踪系统，通常在启动时调用一次，此调用返回后，追踪系统可以接受数据
	configTracing func(serviceName string, options *tracing.Options) (io.Closer, error)
// 暴露Mixer自我监控信息的HTTP服务
	startMonitor  func(port uint16, enableProfiling bool, lf listenFunc) (*monitor, error)
// 监听本地端口并返回一个监听器
	listen        listenFunc
// 配置Istio的日志子系统
	configLog     func(options *log.Options) error
// 让Runtime开始监听配置变更，每当配置变更，Runtime处理新配置并创建Dispatcher
	runtimeListen func(runtime *runtime.Runtime) error
}</pre>
<div class="blog_h2"><span class="graybg">newServer</span></div>
<p>此方法创建一个新的Mixs服务器，服务器由下面的结构表示：</p>
<pre class="crayon-plain-tag">type Server struct {
// 关闭通道
	shutdown  chan error
// 服务API请求的gRPC服务器
	server    *grpc.Server
// API线程池
	gp        *pool.GoroutinePool
// 适配器线程池
	adapterGP *pool.GoroutinePool
// API网络监听器
	listener  net.Listener
// 监控服务器，此结构包含两个字段，一个是http.Server，一个是关闭通道
	monitor   *monitor
// 用于关闭追踪子系统
	tracer    io.Closer
// 可伸缩的策略检查缓存
	checkCache *checkcache.Cache
// 将入站API调用分发给配置好的适配器
	dispatcher dispatcher.Dispatcher

	livenessProbe  probe.Controller
	readinessProbe probe.Controller
// 管理探针控制器所需要的可用性状态，内嵌
	*probe.Probe
}</pre>
<p>该方法的逻辑如下：</p>
<pre class="crayon-plain-tag">func newServer(a *Args, p *patchTable) (*Server, error) {
// 校验Mixs启动参数
	if err := a.validate(); err != nil {
		return nil, err
	}
// 配置日志子系统
	if err := p.configLog(a.LoggingOptions); err != nil {
		return nil, err
	}

	apiPoolSize := a.APIWorkerPoolSize
	adapterPoolSize := a.AdapterWorkerPoolSize

	s := &amp;Server{}

// 创建线程池
// API 线程池
	s.gp = pool.NewGoroutinePool(apiPoolSize, a.SingleThreaded)
	s.gp.AddWorkers(apiPoolSize)

// 适配器线程池
	s.adapterGP = pool.NewGoroutinePool(adapterPoolSize, a.SingleThreaded)
	s.adapterGP.AddWorkers(adapterPoolSize)

	tmplRepo := template.NewRepository(a.Templates)
// 从适配器名称到adapter.Info的映射
	adapterMap := config.AdapterInfoMap(a.Adapters, tmplRepo.SupportsTemplate)

// 状态探针
	s.Probe = probe.NewProbe()

// gRPC选项
	var grpcOptions []grpc.ServerOption
	grpcOptions = append(grpcOptions, grpc.MaxConcurrentStreams(uint32(a.MaxConcurrentStreams)))
	grpcOptions = append(grpcOptions, grpc.MaxMsgSize(int(a.MaxMessageSize)))
// 一元（请求/应答模式）gRPC请求的服务器端拦截器
	var interceptors []grpc.UnaryServerInterceptor
	var err error

// 如果启用了追踪（tracing.option提供了ZipkinURL、JaegerURL或LogTraceSpans=true）
	if a.TracingOptions.TracingEnabled() {
		s.tracer, err = p.configTracing("istio-mixer", a.TracingOptions)
		if err != nil {
			_ = s.Close()
			return nil, fmt.Errorf("unable to setup tracing")
		}
// 则添加基于OpenTracing的追踪拦截器
		interceptors = append(interceptors, otgrpc.OpenTracingServerInterceptor(ot.GlobalTracer()))
	}
// OpenTracing、Prometheus监控拦截器，都来自项目https://github.com/grpc-ecosystem
// 将Prometheus拦截器添加到末尾
	interceptors = append(interceptors, grpc_prometheus.UnaryServerInterceptor)
// 启用Prometheus时间直方图记录，RPC调用的耗时会被记录。Prometheus持有、查询Histogram指标的成本比较高
// 生成的指标都是面向gRPC协议的、通用的，不牵涉Istio的逻辑。指标名以grpc_开头
	grpc_prometheus.EnableHandlingTimeHistogram()
// 将所有拦截器串连为单个拦截器，并添加到gRPC选项
	grpcOptions = append(grpcOptions, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(interceptors...)))

	network := "tcp"
	address := fmt.Sprintf(":%d", a.APIPort)
	if a.APIAddress != "" {
		idx := strings.Index(a.APIAddress, "://")
		if idx &lt; 0 {
			address = a.APIAddress
		} else {
			network = a.APIAddress[:idx]
			address = a.APIAddress[idx+3:]
		}
	}

	if network == "unix" {
// 如果监听UDS，则移除先前的文件
		if err = os.Remove(address); err != nil &amp;&amp; !os.IsNotExist(err) {
			// 除了文件未找到以外的错误，都不允许
			return nil, fmt.Errorf("unable to remove unix://%s: %v", address, err)
		}
	}
// 调用net.Listen监听
	if s.listener, err = p.listen(network, address); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to listen: %v", err)
	}
// ConfigStore用于测试目的，通常都会使用ConfigStoreURL（例如k8s:///home/alex/.kube/config）
	st := a.ConfigStore
	if st != nil &amp;&amp; a.ConfigStoreURL != "" {
		_ = s.Close()
		return nil, fmt.Errorf("invalid arguments: both ConfigStore and ConfigStoreURL are specified")
	}

	if st == nil {
		configStoreURL := a.ConfigStoreURL
		if configStoreURL == "" {
			configStoreURL = "k8s://"
		}
// Registry存储URL scheme与后端实现之间的对应关系

		reg := store.NewRegistry(config.StoreInventory()...)
		groupVersion := &amp;schema.GroupVersion{Group: crd.ConfigAPIGroup, Version: crd.ConfigAPIVersion}
// 创建一个Store实例，它持有Backend，Backend代表一个无类型的Mixer存储后端 —— 例如K8S
// 默认情况下，configStoreURL的Scheme为k8s，Istio会调用config/crd.NewStore
// 传入configStoreURL、GroupVersion、criticalKinds 来创建Backend
		if st, err = reg.NewStore(configStoreURL, groupVersion, rc.CriticalKinds()); err != nil {
			_ = s.Close()
			return nil, fmt.Errorf("unable to connect to the configuration server: %v", err)
		}
	}

	var rt *runtime.Runtime
// 所有模板，目标决定了各分类的适配器（例如所有metric类适配器）在运行时需要处理的数据类型
	templateMap := make(map[string]*template.Info, len(a.Templates))
	for k, v := range a.Templates {
		t := v
		templateMap[k] = &amp;t
	}
// 创建运行时，传入存储、模板、适配器、线程池、是否启用追踪等信息
	rt = p.newRuntime(st, templateMap, adapterMap, a.ConfigDefaultNamespace,
		s.gp, s.adapterGP, a.TracingOptions.TracingEnabled())

// 监听配置存储的变更，初始化配置
	if err = p.runtimeListen(rt); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to listen: %v", err)
	}

// 等待配置存储同步完成
	log.Info("Awaiting for config store sync...")
	if err := st.WaitForSynced(30 * time.Second); err != nil {
		return nil, err
	}

// 设置分发器，分发器负责将API请求分发给配置好的适配器处理
	s.dispatcher = rt.Dispatcher()

// 如果启用了策略检查缓存，则创建LRU缓存对象
	if a.NumCheckCacheEntries &gt; 0 {
		s.checkCache = checkcache.New(a.NumCheckCacheEntries)
	}

// 此全局变量决定是否利用包golang.org/x/net/trace进行gRPC调用追踪
	grpc.EnableTracing = a.EnableGRPCTracing

// 节流阀，限制调用频度
	throttler := loadshedding.NewThrottler(a.LoadSheddingOptions)
// Evaluator方法根据名称返回配置好的LoadEvaluator
// LoadEvaluator能够评估请求是否超过阈值
	if eval := throttler.Evaluator(loadshedding.GRPCLatencyEvaluatorName); eval != nil {
		grpcOptions = append(grpcOptions, grpc.StatsHandler(eval.(*loadshedding.GRPCLatencyEvaluator)))
	}

// 创建gRPC服务器
	s.server = grpc.NewServer(grpcOptions...)
// 注册服务到gRPC服务器
// 注册时需要提供grpc.ServiceDesc，其中包含服务名、方法集合（方法名到处理函数的映射
// api.NewGRPCServer返回 mixerpb.MixerServer 接口，它仅仅包含Check / Report两个方法
	mixerpb.RegisterMixerServer(s.server, api.NewGRPCServer(s.dispatcher, s.gp, s.checkCache, throttler))

// 探针
	if a.LivenessProbeOptions.IsValid() {
		s.livenessProbe = probe.NewFileController(a.LivenessProbeOptions)
		s.RegisterProbe(s.livenessProbe, "server")
		s.livenessProbe.Start()
	}

	if a.ReadinessProbeOptions.IsValid() {
		s.readinessProbe = probe.NewFileController(a.ReadinessProbeOptions)
		rt.RegisterProbe(s.readinessProbe, "dispatcher")
		st.RegisterProbe(s.readinessProbe, "store")
		s.readinessProbe.Start()
	}

// 启动监控服务
	if s.monitor, err = p.startMonitor(a.MonitoringPort, a.EnableProfiling, p.listen); err != nil {
		_ = s.Close()
		return nil, fmt.Errorf("unable to setup monitoring: %v", err)
	}
// 启动ControlZ监听器
	go ctrlz.Run(a.IntrospectionOptions, nil)

	return s, nil
}</pre>
<div class="blog_h2"><span class="graybg">p.newRuntime</span></div>
<p>patchTable的newRuntime函数会调用runtime.New，创建一个新的Mixer运行时 —— Mixer运行时环境的主要入口点，负责监听配置、实例化Handler、创建分发机制（dispatch machinery）、处理请求：</p>
<pre class="crayon-plain-tag">func New(
	s store.Store,
	templates map[string]*template.Info,
	adapters map[string]*adapter.Info,
	defaultConfigNamespace string,
	executorPool *pool.GoroutinePool,
	handlerPool *pool.GoroutinePool,
	enableTracing bool) *Runtime {

	// Ephemeral表示一个短暂的配置状态，它可以被入站配置变更事件所更新
	// Ephemeral本身包含的数据没有价值，你必须调用它的BuildSnapshot方法来创建稳定的、完全解析的配置的快照
	e := config.NewEphemeral(templates, adapters)
	rt := &amp;Runtime{
// 默认配置命名空间
		defaultConfigNamespace: defaultConfigNamespace,
// 短暂配置状态
		ephemeral:              e,
// 配置快照
		snapshot:               config.Empty(),
// 适配器处理器列表
		handlers:               handler.Empty(),
// API请求分发器，需要协程池
		dispatcher:             dispatcher.New(executorPool, enableTracing),
// 适配器处理器的协程池
		handlerPool:            handlerPool,
		Probe:                  probe.NewProbe(),
		store:                  s,
	}

// 从ephemeral构建出新c.snapshot、新c.handlers、新路由表（用于解析入站请求并将其路由给适当的处理器）
// 然后替换路由表，最后清理上一次配置对应的处理器
	rt.processNewConfig()
// 设置探针结果为：尚未监听存储
	rt.Probe.SetAvailable(errNotListening)

	return rt
}</pre>
<div class="blog_h2"><span class="graybg">p.runtimeListen</span></div>
<p>创建Runtime之后，p.runtimeListen被调用。此函数会调用Runtime.StartListening方法来监听配置的变更，同样会立即触发processNewConfig调用。之后，processNewConfig调用会通过store.WatchChanges的回调反复发生。</p>
<pre class="crayon-plain-tag">func (c *Runtime) StartListening() error {
// Runtime的状态锁
	c.stateLock.Lock()
	defer c.stateLock.Unlock()

	if c.shutdown != nil {
		return errors.New("already listening")
	}

// 生成adapter、template等对象类型到它的proto消息的映射（合并到一个映射中）
// adapter.Info.DefaultConfig、template.Info.CtrCfg，以及
// &amp;configpb.Rule{}、&amp;configpb.AttributeManifest{}、&amp;v1beta1.Info{} ...
// 都实现了proto.Message接口
	kinds := config.KindMap(c.snapshot.Adapters, c.snapshot.Templates)
// 开始监控存储，返回当前资源集（key到spec的映射）、监控用的通道
	data, watchChan, err := store.StartWatch(c.store, kinds)
	if err != nil {
		return err
	}

// 设置并覆盖相同的临时状态，其实就是把ephemeral.entries = data
	c.ephemeral.SetState(data)
// 处理新配置
	c.processNewConfig()
// 初始化运行时的关闭通道
	c.shutdown = make(chan struct{})
// 增加一个计数
	c.waitQuiesceListening.Add(1)
	go func() {
// 只有shutdown通道关闭，此监控配置存储变化的循环才会退出
// 当有新的配置变更被发现后，调用onConfigChange，此方法会导致processNewConfig
		store.WatchChanges(watchChan, c.shutdown, watchFlushDuration, c.onConfigChange)
// shutdown通道关闭后，
		c.waitQuiesceListening.Done()
	}()
// 重置可用性状态，此等待组不再阻塞，StopListening方法可以顺利返回
	c.Probe.SetAvailable(nil)

	return nil
}</pre>
<div class="blog_h2"><span class="graybg">onConfigChange</span></div>
<p>当配置存储有变化后，Runtime的该方法会被调用，它的逻辑很简单：</p>
<pre class="crayon-plain-tag">func (c *Runtime) onConfigChange(events []*store.Event) {
// 更新或者擅长ephemeral.entries中的条目
	c.ephemeral.ApplyEvent(events)
// 对最新的配置进行处理
	c.processNewConfig()default
}</pre>
<div class="blog_h2"><span class="graybg">processNewConfig </span></div>
<p>Runtime的processNewConfig方法负责处理从配置存储（K8S）中<span style="background-color: #c0c0c0;">拉取的最新CR，然后创建配置快照、创建处理器表、路由表，并改变Dispatcher的路由</span>：</p>
<pre class="crayon-plain-tag">func (c *Runtime) processNewConfig() {
// 构建一个稳定的、完全解析的配置的快照
	newSnapshot, _ := c.ephemeral.BuildSnapshot()
// 当前运行时使用的处理器
	oldHandlers := c.handlers
// 创建新的处理器表
	newHandlers := handler.NewTable(oldHandlers, newSnapshot, c.handlerPool)
// 返回ExpressionBuilder，用于创建一系列预编译表达式
	builder := compiled.NewBuilder(newSnapshot.Attributes)
// 构建并返回路由表，路由表决定了什么条件下调用什么适配器
	newRoutes := routing.BuildTable(
		newHandlers, newSnapshot, builder, c.defaultConfigNamespace, log.DebugEnabled())

// 改变分发器的路由，分发器负责基于路由表来调用适配器
	oldContext := c.dispatcher.ChangeRoute(newRoutes)

// 修改实例变量
	c.handlers = newHandlers
	c.snapshot = newSnapshot

	log.Debugf("New routes in effect:\n%s", newRoutes)

// 关闭旧的处理器，注意处理器实现了io.Closer接口，这个接口由Istio自己负责，和适配器开发无关
	cleanupHandlers(oldContext, oldHandlers, newHandlers, maxCleanupDuration)
}</pre>
<div class="blog_h2"><span class="graybg">e.BuildSnapshot</span></div>
<p>该方法生成一个完全解析的（没有任何外部依赖）的配置快照。<span style="background-color: #c0c0c0;">快照主要包含静态、动态模板/适配器信息、以及规则信息</span>：</p>
<pre class="crayon-plain-tag">func (e *Ephemeral) BuildSnapshot() (*Snapshot, error) {
	errs := &amp;multierror.Error{}
// 下一个快照的ID
	id := e.nextID
	e.nextID++

	log.Debugf("Building new config.Snapshot: id='%d'", id)

// 一组和istio本身状态监控有关的Prometheus计数器
	counters := newCounters(id)

	e.lock.RLock()

// 处理属性清单，获得属性列表。清单来源有三个地方：
// 1、配置存储中attributemanifest类型的CR。第一次调用该方法时，尚未加载这些CR
// 2、自动生成的template.Info.AttributeManifests
// 注意清单中每个属性，都具有全网格唯一的名称
	attributes := e.processAttributeManifests(counters, errs)

// 处理静态适配器的处理器配置 —— 各种适配器的CR/实例，获得处理器（HandlerStatic）列表
// 对于从配置存储加载的资源，如果在自动生成的adapter.Info中找到对应条目，则认为是合法的处理器
// 对于每个处理器，会创建HandlerStatic结构，此结构表示基于Compiled-in的适配器的处理器
	shandlers := e.processStaticAdapterHandlerConfigs(counters, errs)

// 返回属性描述符查找器（AttributeDescriptorFinder）
	af := ast.NewFinder(attributes)
// 处理静态模板的实例配置 —— 各种模板的CR，获得实例（InstanceStatic）列表
// 对于从配置存储加载的资源，如果在自动生成的template.Info中找到对应条目，则认为是合法的实例
// 对于每个实例，会创建InstanceStatic结构，此结构表示基于Compiled-in的模板的Instance
	instances := e.processInstanceConfigs(af, counters, errs)

// 开始处理动态资源，所谓动态资源，是指没有特定CRD的模板（也就没有对应CR的实例）
// 以及没有特定CRD的适配器（也就没有对应CR的处理器）
// 动态模板注册为template类型的CR
	dTemplates := e.processDynamicTemplateConfigs(counters, errs)
// 动态适配器注册为adapter类型的CR
	dAdapters := e.processDynamicAdapterConfigs(dTemplates, counters, errs)
// 动态处理器注册为handler类型的CR，它必须引用某个adapter的名称
	dhandlers := e.processDynamicHandlerConfigs(dAdapters, counters, errs)
// 动态处理器注册为instance类型的CR，它必须引用某个template的名称
	dInstances := e.processDynamicInstanceConfigs(dTemplates, af, counters, errs)

// 处理规则，规则可以引用上述的静态和动态资源
	rules := e.processRuleConfigs(shandlers, instances, dhandlers, dInstances, af, counters, errs)

// 构建配置快照
	s := &amp;Snapshot{
		ID:                id,
		Templates:         e.templates,
		Adapters:          e.adapters,
		TemplateMetadatas: dTemplates,
		AdapterMetadatas:  dAdapters,
		Attributes:        ast.NewFinder(attributes),
		HandlersStatic:    shandlers,
		InstancesStatic:   instances,
		Rules:             rules,

		HandlersDynamic:  dhandlers,
		InstancesDynamic: dInstances,

		Counters: counters,
	}
	e.lock.RUnlock()

	return s, errs.ErrorOrNil()
}</pre>
<div class="blog_h1"><span class="graybg">适配器初始化过程</span></div>
<p>适配器的初始化过程，是Mixer服务器初始化的一部分。在Mixer服务器启动过程中有如下逻辑：</p>
<pre class="crayon-plain-tag">adapterMap := config.AdapterInfoMap(a.Adapters, tmplRepo.SupportsTemplate)</pre>
<p> 该方法会生成得到所有适配器的adaptor.Info对象：</p>
<pre class="crayon-plain-tag">type Info struct {
	// 适配器的正式名称，必须是RFC 1035兼容的DNS标签
	// 此名称会用在Istio配置中，因此应当简短而具有描述性
	Name string
	// 实现此适配器的包，例如
	// istio.io/istio/mixer/adapter/denier
	Impl string
	// 人类可读的适配器的描述信息
	Description string
	// 该函数指针能够创建一个新的HandlerBuilder，HandlerBuilder能够创建出此适配器的Handler
	NewBuilder NewBuilderFn
	// 此适配器声明支持的模板
	SupportedTemplates []string
	// 传递给HandlerBuilder.Build的适配器的默认参数
	DefaultConfig proto.Message
}</pre>
<p>入参a.Adapters来自supportedAdapters()，此函数是自动生成的。a.Adapters的每个元素的类型是<span style="background-color: #c0c0c0;">adapter.InfoFn。调用此函数即得到对应的adaptor.Info对象</span>：</p>
<pre class="crayon-plain-tag">type InfoFn func() Info</pre>
<p>config.AdapterInfoMap的主要逻辑就是调用各种适配器的adapter.InfoFn方法，并且对adaptor.Info进行各种校验。例如检查它的NewBuilder、NewBuilder字段是否为非空，检查它是否和声明支持的模板兼容。</p>
<p>适配器如果需要初始化，那么<span style="background-color: #c0c0c0;">初始化逻辑就发生在InfoFn中</span>。</p>
<div class="blog_h2"><span class="graybg">Prometheus</span></div>
<p>本节以Prometheus适配器为例，了解适配器的初始化过程。</p>
<div class="blog_h3"><span class="graybg">初始化Info</span></div>
<pre class="crayon-plain-tag">const (
	metricsPath = "/metrics"
// Istio会暴露三个和Prometheus Exporter端口：
// istio-mixer.istio-system:42422，所有由Mixer的Prometheus适配器生成的网格指标
// istio-mixer.istio-system:9093，用于监控Mixer自身的指标
// istio-mixer.istio-system:9102，Envoy生成的原始统计信息，从Statsd转换为Prometheus格式
	defaultAddr = ":42422"
)

func GetInfo() adapter.Info {
	ii, _ := GetInfoWithAddr(defaultAddr)
	return ii
}</pre>
<p>GetInfoWithAddr方法的实现如下：</p>
<pre class="crayon-plain-tag">func GetInfoWithAddr(addr string) (adapter.Info, Server) {
// HandlerBuilder单例
	singletonBuilder := &amp;builder{
// HTTP服务器，这里不会启动监听
		srv: newServer(addr),
	}
// 创建注册表singletonBuilder.registry = prometheus.NewPedanticRegistry()
// 情况指标信息 singletonBuilder.metrics = make(map[string]*cinfo)
	singletonBuilder.clearState()
// 返回adaptor.Info对象
	return adapter.Info{
		Name:        "prometheus",
		Impl:        "istio.io/istio/mixer/adapter/prometheus",
		Description: "Publishes prometheus metrics",
		SupportedTemplates: []string{
			metric.TemplateName,
		},
		NewBuilder:    func() adapter.HandlerBuilder { return singletonBuilder },
		DefaultConfig: &amp;config.Params{},
	}, singletonBuilder.srv
}</pre>
<div class="blog_h3"><span class="graybg">初始化Handler </span></div>
<p>每当配置变更后，适配器的Handler会被初始化。Runtime.processNewConfig会调用：</p>
<pre class="crayon-plain-tag">newHandlers := handler.NewTable(oldHandlers, newSnapshot, c.handlerPool)</pre>
<p>创建handler.Table，此表包含了所有实例化的、配置好的适配器的处理器的信息：</p>
<pre class="crayon-plain-tag">type Table struct {
// 表格条目
	entries map[string]Entry

	counters tableCounters
}

// 单个处理器
type Entry struct {
	// 处理器的名称
	Name string

	// 处理器对象
	Handler adapter.Handler

	// 适配器名称
	AdapterName string

	// 创建此处理器使用的适配器配置（参数）的签名信息
	Signature signature

	// 传递给处理器的adapter.Env
	env env
}</pre>
<p>每个适配器可以消费多个实例，对于适配器和实例的每个组合，handler.NewTable方法会为其创建Handler：</p>
<pre class="crayon-plain-tag">// 适配器实例 - 模板实例的映射
// map[*HandlerStatic][]*InstanceStatic
instancesByHandler := config.GetInstancesGroupedByHandlers(snapshot)
// map[*HandlerDynamic][]*InstanceDynamic
instancesByHandlerDynamic := config.GetInstancesGroupedByHandlersDynamic(snapshot)

// 表
t := &amp;Table{
	entries:  make(map[string]Entry, len(instancesByHandler)+len(instancesByHandlerDynamic)),
	counters: newTableCounters(snapshot.ID),
}

// 对于每个静态处理器 - 实例组合
for handler, instances := range instancesByHandler {
        // 为其创建条目，并加入到表中
	createEntry(old, t, handler, instances, snapshot.ID,
// 这个回调用于用于创建处理器
		func(handler hndlr, instances interface{}) (h adapter.Handler, e env, err error) {
// 环境信息
			e = NewEnv(snapshot.ID, handler.GetName(), gp).(env)
// 创建出处理器
			h, err = config.BuildHandler(handler.(*config.HandlerStatic), instances.([]*config.InstanceStatic), e, snapshot.Templates)
			return h, e, err
		})
}

// 对于每个动态处理器 - 实例组合
for handler, instances := range instancesByHandlerDynamic {
	createEntry(old, t, handler, instances, snapshot.ID, ...
}</pre>
<p>config.BuildHandler经过几层转发，最终会调用Prometheus适配器的方法：</p>
<pre class="crayon-plain-tag">func (b *builder) Build(ctx context.Context, env adapter.Env) (adapter.Handler, error) {

	cfg := b.cfg
	var metricErr *multierror.Error

// 用于收集指标配置
	newMetrics := make([]*config.Params_MetricInfo, 0, len(cfg.Metrics))

	// 检查指标是否被重新定义，也就是对应的CR是否被修改
	// 如果是，则清空指标注册表、指标映射。重定义会导致Prometheus客户端Panic
	// 添加、移除则没有问题
	var cl *cinfo
// 遍历新配置的指标列表
	for _, m := range cfg.Metrics {
		// 当前指标表中没有匹配项，加入
		if cl = b.metrics[m.InstanceName]; cl == nil {
			newMetrics = append(newMetrics, m)
			continue
		}

		// 散列值没有变，和之前的指标配置一样
		if cl.sha == computeSha(m, env.Logger()) {
			continue
		}

		// 散列值不匹配，发生了重定义。适配器需要重现加载
		env.Logger().Warningf("Metric %s redefined. Reloading adapter.", m.Name)
		// 重建注册表、清空指标信息
		b.clearState()
		// 将所有指标作为新配置看待
		newMetrics = cfg.Metrics
		break
	}

	env.Logger().Debugf("%d new metrics defined", len(newMetrics))

// 遍历处理所有新指标
	var err error
	for _, m := range newMetrics {
		ns := defaultNS
		if len(m.Namespace) &gt; 0 {
			ns = safeName(m.Namespace)
		}
// 指标全名，即CR的名称
		mname := m.InstanceName
		if len(m.Name) != 0 {
// 转换为短名
			mname = m.Name
		}
// 构建出指标信息cinfo
		ci := &amp;cinfo{kind: m.Kind, sha: computeSha(m, env.Logger())}
		ci.sortedLabels = make([]string, len(m.LabelNames))
		copy(ci.sortedLabels, m.LabelNames)
		sort.Strings(ci.sortedLabels)

// 根据指标类型的不同，分别处理。逻辑都是注册指标到注册表
		switch m.Kind {
		case config.GAUGE:
			ci.c, err = registerOrGet(b.registry, newGaugeVec(ns, mname, m.Description, m.LabelNames))
			b.metrics[m.InstanceName] = ci
		case config.COUNTER:
			ci.c, err = registerOrGet(b.registry, newCounterVec(ns, mname, m.Description, m.LabelNames))
			b.metrics[m.InstanceName] = ci
		case config.DISTRIBUTION:
			ci.c, err = registerOrGet(b.registry, newHistogramVec(ns, mname, m.Description, m.LabelNames, m.Buckets))
			b.metrics[m.InstanceName] = ci
		default:
			metricErr = multierror.Append(metricErr, fmt.Errorf("unknown metric kind (%d); could not register metric %v", m.Kind, m))
		}
	}

// 启动Exporter的HTTP服务器，如果已经启动则不管
	if err := b.srv.Start(env, promhttp.HandlerFor(b.registry, promhttp.HandlerOpts{})); err != nil {
		return nil, err
	}

// 如果配置了指标过期功能，则定期删除老旧指标
	var expiryCache cache.ExpiringCache
	if cfg.MetricsExpirationPolicy != nil {
		checkDuration := cfg.MetricsExpirationPolicy.ExpiryCheckIntervalDuration
		if checkDuration == 0 {
			checkDuration = cfg.MetricsExpirationPolicy.MetricsExpiryDuration / 2
		}
		expiryCache = cache.NewTTLWithCallback(
			cfg.MetricsExpirationPolicy.MetricsExpiryDuration,
			checkDuration,
			deleteOldMetrics)
	}

	return &amp;handler{b.srv, b.metrics, expiryCache}, metricErr.ErrorOrNil()
}</pre>
<div class="blog_h3"><span class="graybg">暴露指标</span></div>
<p>b.srv.Start会启动作为Exporter的HTTP服务器： </p>
<pre class="crayon-plain-tag">func (s *serverInst) Start(env adapter.Env, metricsHandler http.Handler) (err error) {
// 加锁保护
	s.lock.Lock()
	defer s.lock.Unlock()

	// 如果服务器已经启动了，则委托
	// just switch the delegate handler.
	if s.srv != nil {
		s.refCnt++
		s.handler.setDelegate(metricsHandler)
		return nil
	}
// 否则，创建监听
	listener, err := net.Listen("tcp", s.addr)
	s.port = listener.Addr().(*net.TCPAddr).Port
// 配置ServerMux
	srvMux := http.NewServeMux()
	s.handler = &amp;metaHandler{delegate: metricsHandler}
	srvMux.Handle(metricsPath, s.handler)
	srv := &amp;http.Server{Addr: s.addr, Handler: srvMux}
// 在后台运行
	env.ScheduleDaemon(func() {
// 开始监听
		env.Logger().Infof("serving prometheus metrics on %d", s.port)
		if err := srv.Serve(listener.(*net.TCPListener)); err != nil {
			if err == http.ErrServerClosed {
				env.Logger().Infof("HTTP server stopped")
			} else {
				_ = env.Logger().Errorf("prometheus HTTP server error: %v", err) 
			}
		}
	})
	s.srv = srv
	s.refCnt++

	return nil
} </pre>
<p>使用Istio官方Chart安装时，其内置的Prometheus服务器会自动采集该HTTP服务器暴露的指标。</p>
<div class="blog_h1"><span class="graybg">Mixs处理请求过程 </span></div>
<p>在运行期间，Envoy代理会向Mixer服务发起CHECK/REPORT/QUOTA等调用。Mixer会将这些请求转发给匹配的适配器进行处理。</p>
<p>本节以Prometheus适配器为例，说明REPORT请求的处理过程。</p>
<div class="blog_h2"><span class="graybg">相关配置</span></div>
<p>以官方Chart部署Istio时，会创建如下Rule：</p>
<pre class="crayon-plain-tag">apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: promhttp
spec:
  actions:
  - handler: handler.prometheus
    instances:
    - requestcount.metric
    - requestduration.metric
    - requestsize.metric
    - responsesize.metric
  match: context.protocol == "http" || context.protocol == "grpc"</pre>
<p>这里我们测试requestcount这个指标，和它相关的Handler、Instance配置片断如下：</p>
<pre class="crayon-plain-tag"># Handler
apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
  name: handler
  namespace: istio-system
spec:
  metrics:
  - instance_name: requestcount.metric.istio-system
    kind: COUNTER
    label_names:
    - reporter
    - source_app
    - source_principal
    - source_workload
    - source_workload_namespace
    - source_version
    - destination_app
    - destination_principal
    - destination_workload
    - destination_workload_namespace
    - destination_version
    - destination_service
    - destination_service_name
    - destination_service_namespace
    - request_protocol
    - response_code
    - connection_security_policy
    name: requests_total

# Instance
kind: metric
metadata:
  name: requestcount
spec:
  dimensions:
    connection_security_policy: conditional((context.reporter.kind | "inbound") == "outbound", "unknown", conditional(connection.mtls | false, "mutual_tls", "none"))
    destination_app: destination.labels["app"] | "unknown"
    destination_principal: destination.principal | "unknown"
    destination_service: destination.service.host | "unknown"
    destination_service_name: destination.service.name | "unknown"
    destination_service_namespace: destination.service.namespace | "unknown"
    destination_version: destination.labels["version"] | "unknown"
    destination_workload: destination.workload.name | "unknown"
    destination_workload_namespace: destination.workload.namespace | "unknown"
    reporter: conditional((context.reporter.kind | "inbound") == "outbound", "source", "destination")
    request_protocol: api.protocol | context.protocol | "unknown"
    response_code: response.code | 200
    source_app: source.labels["app"] | "unknown"
    source_principal: source.principal | "unknown"
    source_version: source.labels["version"] | "unknown"
    source_workload: source.workload.name | "unknown"
    source_workload_namespace: source.workload.namespace | "unknown"
  monitored_resource_type: '"UNSPECIFIED"'
  value: "1"</pre>
<div class="blog_h2"><span class="graybg">发送请求</span></div>
<p>要触发Mixer服务器端的处理逻辑，不需要运行Envoy代理，调用命令行客户端mixc就可以了。  </p>
<p>为了匹配上面的promhttp规则，我们需要发送一个属性context.protocol的值为http的REPORT请求：</p>
<pre class="crayon-plain-tag">mixc report -m localhost:9091 \
    -t request.time=2019-03-27T11:00:00.000Z,response.time=2019-03-27T11:00:00.900Z  \
    -a context.protocol=http,context.reporter.kind=outbound,source.namespace=default  \
    -a destination.service=kubernetes

# 2019-03-27T03:52:05.237085Z     info    parsed scheme: ""
# 2019-03-27T03:52:05.237179Z     info    scheme "" not registered, fallback to default scheme
# 2019-03-27T03:52:05.237532Z     info    ccResolverWrapper: sending new addresses to cc: [{localhost:9091 0  &lt;nil&gt;}]
# 2019-03-27T03:52:05.237592Z     info    ClientConn switching balancer to "pick_first"
# 2019-03-27T03:52:05.237768Z     info    pickfirstBalancer: HandleSubConnStateChange: 0xc0001940b0, CONNECTING
# 2019-03-27T03:52:05.237788Z     info    blockingPicker: the picked transport is not ready, loop back to repick
# 2019-03-27T03:52:05.241228Z     info    pickfirstBalancer: HandleSubConnStateChange: 0xc0001940b0, READY
# Report RPC returned OK</pre>
<div class="blog_h2"><span class="graybg">拦截请求</span></div>
<div class="blog_h3"><span class="graybg">gRPC接口</span></div>
<p>Mixer处理请求的接口由以下Proto文件定义：</p>
<pre class="crayon-plain-tag">service Mixer {
  // 进行先决条件检查，或者进行配额
  rpc Check(CheckRequest) returns (CheckResponse) {}

  // 遥测报告
  rpc Report(ReportRequest) returns (ReportResponse) {}
}</pre>
<div class="blog_h3"><span class="graybg">Prometheus拦截器</span></div>
<p>通过前面章节的源码分析，我们了解到，在Mixer服务启动时，注册了OpenTracing、Prometheus的gRPC拦截器。因此首先会执行Prometheus拦截器：</p>
<pre class="crayon-plain-tag">// 自动生成的代码
func _Mixer_Report_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(ReportRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
// 没有拦截器，直接调用MixerServer实现
	if interceptor == nil {
		return srv.(MixerServer).Report(ctx, in)
	}
	info := &amp;grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: "/istio.mixer.v1.Mixer/Report",
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(MixerServer).Report(ctx, req.(*ReportRequest))
	}
// 实际上是有拦截器的，调用拦截器，通过拦截器再调用MixerServer实现
	return interceptor(ctx, in, info, handler)
}</pre>
<p>来自<a href="https://github.com/grpc-ecosystem/go-grpc-prometheus">go-grpc-prometheus</a>项目的Prometheus拦截器，逻辑如下：</p>
<pre class="crayon-plain-tag">func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// grpc_server_started_total指标
	monitor := newServerReporter(Unary, info.FullMethod)
// grpc_server_msg_received_total指标
	monitor.ReceivedMessage()
// 调用MixerServer实现
	resp, err := handler(ctx, req)
// grpc_server_handled_total指标
// grpc_server_handling_seconds指标，直方图
	monitor.Handled(grpc.Code(err))
	if err == nil {
// grpc_server_msg_sent_total
		monitor.SentMessage()
	}
	return resp, err
}</pre>
<div class="blog_h2"><span class="graybg">处理请求</span></div>
<div class="blog_h3"><span class="graybg">Report</span></div>
<p>MixerServer接口的实现定义在api.grpcServer结构中。Report方法会逐个处理每条消息，并进行：</p>
<ol>
<li>预处理：调用匹配的属性生成处理器</li>
<li>处理：调用匹配的主处理器</li>
</ol>
<p>注意，单次Mixer请求可以携带多条消息，每条消息都对应Envoy代理处理的一个实际请求。</p>
<p>Report方法的实现如下：</p>
<pre class="crayon-plain-tag">func (s *grpcServer) Report(ctx context.Context, req *mixerpb.ReportRequest) (*mixerpb.ReportResponse, error) {
// 限流逻辑，默认情况下Mixer的限流是关闭的
// req.Attributes的类型是 []v1.CompressedAttributes，每个元素表示报告的一条信息，客户端可以一次报送多条信息
// 但是对于非REPORT请求，每次只能有一条消息
	if s.throttler.Throttle(loadshedding.RequestInfo{PredictedCost: float64(len(req.Attributes))}) {
		return nil, grpc.Errorf(codes.Unavailable, "Server is currently overloaded. Please try again.")
	}

	if len(req.Attributes) == 0 {
		// 没有报告任何东西
		return reportResp, nil
	}

// Words表示消息级别的字典 —— 属性名的数组、字符串属性值
	for i := 0; i &lt; len(req.Attributes); i++ {
		if len(req.Attributes[i].Words) == 0 {
// req.DefaultWords为所有消息的默认字典。可以让请求中多个消息共享字典，进而减少请求大小
			req.Attributes[i].Words = req.DefaultWords
		}
	}

	// bag around the input proto that keeps track of reference attributes
// 创建一个ProtoBag —— 基于属性Proto消息，实现Bag接口（用于访问属性集）
	protoBag := attribute.NewProtoBag(&amp;req.Attributes[0], s.globalDict, s.globalWordList)

// 从对象池中取得一个MutableBag，对象池避免了反复的内存分配，然后将其parent设置为protoBag
// accumBag（请求包requestBag），跟踪除了第一个以外，所有消息相对于第一个的delta
	accumBag := attribute.GetMutableBag(protoBag)

// reportBag（响应包responseBag），持有预处理之后的输出状态，预处理适配器可能会生成一些新属性，这些新属性以delta的形式存储在此
	reportBag := attribute.GetMutableBag(accumBag)

// 基于GlobalTracer，启动并返回操作名称（operationName）为Report的Span，使用从ctx中找到的Span作为ChildOfRef
// 如果找不到作为parent的Span，则创建一个根Span
	reportSpan, reportCtx := opentracing.StartSpanFromContext(ctx, "Report")
// 从对象池中获得reporter，为其提供路由上下文（report.rc）、报告上下文（r.ctx，其中包含了Trace树的信息）
	reporter := s.dispatcher.GetReporter(reportCtx)

	var errors *multierror.Error
/* 开始逐个处理消息 */
	for i := 0; i &lt; len(req.Attributes); i++ {
// 以Report为父Span，依次创建子Span： attribute bag N
		span, newctx := opentracing.StartSpanFromContext(reportCtx, fmt.Sprintf("attribute bag %d", i))

// 第一个属性块（消息）作为protoBag的基础，计算每个子包的delta
		if i &gt; 0 {
			if err := accumBag.UpdateBagFromProto(&amp;req.Attributes[i], s.globalWordList); err != nil {
				err = fmt.Errorf("request could not be processed due to invalid attributes: %v", err)
// 为子Span记录字段，然后结束Span
				span.LogFields(otlog.String("error", err.Error()))
				span.Finish()
				errors = multierror.Append(errors, err)
				break
			}
		}

		lg.Debug("Dispatching Preprocess")
// 预处理，将请求包分发给那些需要提前执行的适配器，例如属性生成适配器
		if err := s.dispatcher.Preprocess(newctx, accumBag, reportBag); err != nil {
			err = fmt.Errorf("preprocessing attributes failed: %v", err)
			span.LogFields(otlog.String("error", err.Error()))
			span.Finish()
			errors = multierror.Append(errors, err)
			continue
		}

// 主处理，分发给主适配器
		lg.Debug("Dispatching to main adapters after running preprocessors")
		lg.Debuga("Attribute Bag: \n", reportBag)
		lg.Debugf("Dispatching Report %d out of %d", i+1, len(req.Attributes))

		if err := reporter.Report(reportBag); err != nil {
			span.LogFields(otlog.String("error", err.Error()))
			span.Finish()
			errors = multierror.Append(errors, err)
			continue
		}

		span.Finish()

		// 清空包内容，准备处理下一个请求包使用
		reportBag.Reset()
	}
/* 结束逐个处理消息 */

// 重置，并放回对象池
	reportBag.Done()
	accumBag.Done()
	protoBag.Done()

// 刷出，调用reporter.impl.getSession.dispatchBufferedReports()，将之前缓冲的dispatchState全部分发出去
// 然后将会话放回对象池
	if err := reporter.Flush(); err != nil {
		errors = multierror.Append(errors, err)
	}
// 将Reporter对象也放回池中
	reporter.Done()

// 结束Span
	if errors != nil {
		reportSpan.LogFields(otlog.String("error", errors.Error()))
	}
	reportSpan.Finish()

	if errors != nil {
		lg.Errora("Report failed:", errors.Error())
		return nil, grpc.Errorf(codes.Unknown, errors.Error())
	}
// 返回响应
	return reportResp, nil
}</pre>
<div class="blog_h3"><span class="graybg">Preprocess </span></div>
<p>Dispatcher.Preprocess方法负责请求预处理，将请求包分发给那些需要提前执行的适配器，并收集它们产生的属性：</p>
<pre class="crayon-plain-tag">func (d *Impl) Preprocess(ctx context.Context, bag attribute.Bag, responseBag *attribute.MutableBag) error {
// 返回一个session，此结构表示对Dispatcher接口（的实现Impl）的一个调用会话
// 其中包含了处理调用所需的所有可变状态
// getSession从对象池获取一个session对象，然后设置它的
// s.impl，Dispatcher对象
// s.rc，路由上下文对象
// s.ctx 包含Span信息的上下文
// s.variety 需要调用的适配器的种类
// s.bag 请求包
	s := d.getSession(ctx, tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR, bag)
// s.responseBag 响应包
	s.responseBag = responseBag
// 执行分发
	err := s.dispatch()
	if err == nil {
		err = s.err
	}
// 放回对象池
	d.putSession(s)
	return err
}</pre>
<div class="blog_h3"><span class="graybg">dispatch </span></div>
<p>session.dispatch方法真正负责请求包的分发工作：</p>
<pre class="crayon-plain-tag">func (s *session) dispatch() error {
// 根据报告者类型（从context.reporter.kind获取），默认inbound推断命名空间
// inbound 则命名空间为destination.namespace
// outbound 则命名空间为source.namespace
	namespace, err := getIdentityNamespace(s.bag)
	if err != nil {
// 无法获取命名空间，出错
// 更新直方图（Observe一个值）：
// mixer_dispatcher_destinations_per_request
// mixer_dispatcher_instances_per_request
		updateRequestCounters(0, 0)
		log.Warnf("unable to determine identity namespace: '%v', operation='%d'", err, s.variety)
		return err
	}
// 从路由表获得s.variety类型的、namespace命名空间的目的地列表
// 注意：如果当前命名空间没有匹配的目的地，则使用默认配置存储命名空间（istio-system）中定义的目的地
	destinations := s.rc.Routes.GetDestinations(s.variety, namespace)

// 要访问的目标服务 
	destinationService := ""
	v, ok := s.bag.Get("destination.service")
	if ok {
		destinationService = v.(string)
	}
// 创建一个新的Context，携带键值对，以前面的子Span上下文为父，0=adapter.RequestData为键值对
// RequestData定义了关于请求的信息，例如它的目的服务
	ctx := adapter.NewContextWithRequestData(s.ctx, &amp;adapter.RequestData{
		DestinationService: adapter.Service{
			FullName: destinationService,
		},
	})

// 确保能够将请求并行的分发给所有处理器，将s.completed设置为足够大的chan *dispatchState
// 每个chan *dispatchState收集单个目的地的处理结果
	s.ensureParallelism(destinations.Count())

	foundQuota := false
// 构建出的实例数量
	ninputs := 0
// 匹配的目的地数量
	ndestinations := 0
	for _, destination := range destinations.Entries() {
// dispatchState持有和单个目的地相关的输入/输出状态
		var state *dispatchState

// 对于REPORT处理器
		if s.variety == tpb.TEMPLATE_VARIETY_REPORT {
// 生成并缓存分发状态到s.reportStates
			state = s.reportStates[destination]
			if state == nil {
// 从对象池中获取一个dispatchState并对其赋值，对象池在Mixer中大量使用，减少了内存分配
				state = s.impl.getDispatchState(ctx, destination)
				s.reportStates[destination] = state
			}
		}

		for _, group := range destination.InstanceGroups {
// 判断请求包是否和每个实例组匹配
			groupMatched := group.Matches(s.bag)

			if groupMatched {
				ndestinations++
			}

// 遍历每个组中的每个实例，调用其构建器。构建器的逻辑取决于你配置的各种模板实例，例如metric的CR
			for j, input := range group.Builders {
				if s.variety == tpb.TEMPLATE_VARIETY_QUOTA {
// 对于配额适配器，必须要求实例构建器名称和实例名一致
// CRD名称即模板信息名TemplateInfo.Name，例如        logentries
// 实例名，即CR名，例如 kubectl -n istio-system get logentries.config.istio.io
// 得到的accesslog、tcpaccesslog                  
					if !strings.EqualFold(input.InstanceShortName, s.quotaArgs.Quota) {
						continue
					}
					if !groupMatched {
						// 这是一个条件性的配额，并且当前不匹配条件，直接返回请求的额度
						s.quotaResult.Amount = s.quotaArgs.Amount
						s.quotaResult.ValidDuration = defaultValidDuration
					}
					foundQuota = true
				}

				if !groupMatched {
					continue
				}

				var instance interface{}
// 从请求包构建出实例，Builder方法是自动生成的
				if instance, err = input.Builder(s.bag); err != nil {
					log.Errorf("error creating instance: destination='%v', error='%v'", destination.FriendlyName, err)
					s.err = multierror.Append(s.err, err)
					continue
				}
				ninputs++
// 对于REPORT模板，在执行分发前，尽可能的将实例累积到分发状态的instances列表中
				if s.variety == tpb.TEMPLATE_VARIETY_REPORT {
					state.instances = append(state.instances, instance)
					continue
				}

// 对于其它模板类型，直接分发给处理器
				state = s.impl.getDispatchState(ctx, destination)
				state.instances = append(state.instances, instance)
				if s.variety == tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR {
// 属性生成处理器需要处理Mapper —— 将处理器输出映射入主属性集的函数
					state.mapper = group.Mappers[j]
					state.inputBag = s.bag
				}

// 配额模板相关参数
				state.quotaArgs.BestEffort = s.quotaArgs.BestEffort
				state.quotaArgs.DeduplicationID = s.quotaArgs.DeduplicationID
				state.quotaArgs.QuotaAmount = s.quotaArgs.Amount
// 直接分发
				s.dispatchToHandler(state)
			}
		}
	}

// Observe mixer_dispatcher_destinations_per_request
// Observe mixer_dispatcher_instances_per_request
	updateRequestCounters(ndestinations, ninputs)

// 等待所有处理器处理完毕
	s.waitForDispatched()

// 如果当前执行的是配额处理器，且没有找到配额，则警告但是允许访问
	if s.variety == tpb.TEMPLATE_VARIETY_QUOTA &amp;&amp; !foundQuota {
		s.quotaResult.Amount = s.quotaArgs.Amount
		s.quotaResult.ValidDuration = defaultValidDuration
		log.Warnf("Requested quota '%s' is not configured", s.quotaArgs.Quota)
	}

	return nil
}</pre>
<p>需要注意： </p>
<ol>
<li>对于REPORT模板，仅仅是将生成的Instance存放到dispatchState.instances数组中，不分发。延迟到所有请求消息处理完毕后，由Reporter.Flush统一分发</li>
<li>对于CHECK模板，直接调用session.dispatchToHandler进行分发</li>
</ol>
<p>分发不是直接在当前线程调用适配器，而是排队，由协程池的调度循环异步处理：</p>
<pre class="crayon-plain-tag">func (s *session) dispatchToHandler(ds *dispatchState) {
	s.activeDispatches++
	ds.session = s
// 调用协程池，调度一个工作
	s.impl.gp.ScheduleWork(ds.invokeHandler, nil)
}</pre>
<p>dispatchState.invokeHandler方法真正直接调用适配器：</p>
<pre class="crayon-plain-tag">func (ds *dispatchState) invokeHandler(interface{}) {
// 顺利处理完毕，没有Panic
	reachedEnd := false

	defer func() {
		if reachedEnd {
			return
		}
// 从适配器代码导致的Panic中恢复，防止Mixer直接崩了
		r := recover()
		ds.err = fmt.Errorf("panic during handler dispatch: %v", r)
		log.Errorf("%v\n%s", ds.err, debug.Stack())

		if log.DebugEnabled() {
			log.Debugf("stack dump for handler dispatch panic:\n%s", debug.Stack())
		}
// 提示此此目的地的分发处理完毕
		ds.session.completed &lt;- ds
	}()

// 跟踪
	span, ctx, start := ds.beginSpan(ds.ctx)

	log.Debugf("begin dispatch: destination='%s'", ds.destination.FriendlyName)

	switch ds.destination.Template.Variety {
// 属性生成器
	case tpb.TEMPLATE_VARIETY_ATTRIBUTE_GENERATOR:
		ds.outputBag, ds.err = ds.destination.Template.DispatchGenAttrs(
			ctx, ds.destination.Handler, ds.instances[0], ds.inputBag, ds.mapper)
// 前置条件检查
	case tpb.TEMPLATE_VARIETY_CHECK:
		ds.checkResult, ds.err = ds.destination.Template.DispatchCheck(
			ctx, ds.destination.Handler, ds.instances[0])
// 遥测/报告
	case tpb.TEMPLATE_VARIETY_REPORT:
		ds.err = ds.destination.Template.DispatchReport(
			ctx, ds.destination.Handler, ds.instances)
// 配额
	case tpb.TEMPLATE_VARIETY_QUOTA:
		ds.quotaResult, ds.err = ds.destination.Template.DispatchQuota(
			ctx, ds.destination.Handler, ds.instances[0], ds.quotaArgs)
// 无法处理的模板类型，Panic
	default:
		panic(fmt.Sprintf("unknown variety type: '%v'", ds.destination.Template.Variety))
	}

	log.Debugf("complete dispatch: destination='%s' {err:%v}", ds.destination.FriendlyName, ds.err)
// 追踪
	ds.completeSpan(span, time.Since(start), ds.err)
// 将当前目的地设置为分发处理完毕
	ds.session.completed &lt;- ds

	reachedEnd = true
}</pre>
<p>可以看到，上述方法都是把调用委托给目的地的TemplateInfo.Dispatch***函数指针处理的。这些函数指针就是适配器的相应方法。对于Metric模板，Prometheus适配器的方法实现如下：</p>
<pre class="crayon-plain-tag">func (h *handler) HandleMetric(_ context.Context, vals []*metric.Instance) error {
	var result *multierror.Error

// 遍历Instance
	for _, val := range vals {
// 获取该Instance对应的handler（例如requestcount.metric.istio-system）的信息（cinfo）
		ci := h.metrics[val.Name]
		if ci == nil {
			result = multierror.Append(result, fmt.Errorf("could not find metric info from adapter config for %s", val.Name))
			continue
		}
		collector := ci.c
		switch ci.kind {
// 按指标类型分别处理
		case config.GAUGE:
			vec := collector.(*prometheus.GaugeVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "gauge", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
			vec.With(pl).Set(amt)
		case config.COUNTER:
// 转换为指标向量，指标向量的每个元素是具有不同标签值的同一类（名字相同）指标
			vec := collector.(*prometheus.CounterVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "counter", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
// vec.With(pl)返回具有指定标签集的指标对象，这里是Counter，然后加上一个值（在当前时间点）
			vec.With(pl).Add(amt)
		case config.DISTRIBUTION:
// DISTRIBUTION映射为Prometheus类型 Histogram
			vec := collector.(*prometheus.HistogramVec)
			amt, err := promValue(val.Value)
			if err != nil {
				result = multierror.Append(result, fmt.Errorf("could not get value for metric %s: %v", val.Name, err))
				continue
			}
			pl := promLabels(val.Dimensions)
			if h.labelsCache != nil {
				h.labelsCache.Set(key(val.Name, "distribution", pl, ci.sortedLabels), &amp;cacheEntry{vec, pl})
			}
			vec.With(pl).Observe(amt)
		}
	}

	return result.ErrorOrNil()
}


cinfo struct {
// 负责收集指标的接口，gauge counter等都实现了此接口
	c            prometheus.Collector
	sha          [sha1.Size]byte
	kind         config.Params_MetricInfo_Kind
	sortedLabels []string
}</pre>
<div class="blog_h2"><span class="graybg">主要接口</span></div>
<div class="blog_h3"><span class="graybg">Dispatcher</span></div>
<p>将入站的API调用分发给配置的适配器：</p>
<pre class="crayon-plain-tag">type Dispatcher interface {
	// 进行预处理，将请求包分发给那些需要提前执行的适配器，
	// 目前这种适配器主要指属性生成适配器
	Preprocess(ctx context.Context, requestBag attribute.Bag, responseBag *attribute.MutableBag) error

	// 进行CHECK分发，基于CHECK类型模板的Instance，将被转发给感兴趣的适配器
	Check(ctx context.Context, requestBag attribute.Bag) (adapter.CheckResult, error)

	// 获取能够缓冲REPORT请求的报告器
	GetReporter(ctx context.Context) Reporter

	// 进行QUOTA分发
	Quota(ctx context.Context, requestBag attribute.Bag, qma QuotaMethodArgs) (adapter.QuotaResult, error)
}</pre>
<div class="blog_h3"><span class="graybg">Reporter</span></div>
<p>负责产生一系列的报告：</p>
<pre class="crayon-plain-tag">type Reporter interface {
	// 添加一个条目（请求包）到报告状态中
	Report(requestBag attribute.Bag) error

	// 刷出所有缓冲的状态到适当的适配器
	Flush() error

	// 完成Reporter的处理过程
	Done()
}</pre>
<div class="blog_h2"><span class="graybg">主要结构</span></div>
<div class="blog_h3"><span class="graybg">Destination</span></div>
<p>目的地，包含一个目标处理器，以及需要（在满足条件的情况下）发送给它的实例：</p>
<pre class="crayon-plain-tag">type Destination struct {
	// 用于调试的目的地ID
	id uint32

	// 需要调用的处理器
	Handler adapter.Handler

	// 用于监控/日志目的的处理器名称
	HandlerName string

	// 用于监控/日志目的的适配器名称（处理器类型）
	AdapterName string

	// 使用的模板，由于某些适配器支持多种模板，这些适配器可能对应多个Destination
	// 每种模板都有类型，并且定义了支持它的适配器必须实现的接口
	Template *TemplateInfo

	// 实例组，每组实例在满足条件的情况下，会发送给处理器
	InstanceGroups []*InstanceGroup

	// 最大允许的实例数
	maxInstances int

	// 用于监控/日志目的目的地名称
	FriendlyName string

	// 性能计数器
	Counters DestinationCounters
}</pre>
<div class="blog_h3"><span class="graybg">dispatchState</span></div>
<p>此结构用于收集<span style="background-color: #c0c0c0;">单个目的地（适配器+模板组合）</span>的处理状态和结果：</p>
<pre class="crayon-plain-tag">type dispatchState struct {
// 所属的分发调用会话
	session *session
// 上下文，其中包含了OpenTracing的Span信息
	ctx     context.Context
// 目的地
	destination *routing.Destination
// 对于属性生成模板，将模板输出映射入主属性列表的函数
	mapper      template.OutputMapperFn
// 输入包
	inputBag  attribute.Bag
// 配额请求的参数
	quotaArgs adapter.QuotaArgs
// 构建出的，供适配器消费的实例列表
	instances []interface{}

// 处理过程中的错误信息
	err         error
// 输出包
	outputBag   *attribute.MutableBag
// CHECK调用的结果
	checkResult adapter.CheckResult
// QUOTA调用的结果
	quotaResult adapter.QuotaResult
}</pre>
<div class="blog_h3"><span class="graybg">session</span></div>
<p>对一个客户端CHECK/REPORT/QUOTA请求的预处理和主处理的过程，是一个会话。此结构存储相关的信息：</p>
<pre class="crayon-plain-tag">type session struct {
// 拥有此会话的Dispatcher
	impl *Impl

// 本次会话使用的路由上下文
	rc *RoutingContext

// 上下文信息
	ctx          context.Context
// 输入包
	bag          attribute.Bag
// 配额调用的参数
	quotaArgs    QuotaMethodArgs
// 输出包
	responseBag  *attribute.MutableBag
// 报告请求的分发状态
	reportStates map[*routing.Destination]*dispatchState

// CHECK/QUOTA调用的结果
	checkResult adapter.CheckResult
	quotaResult adapter.QuotaResult
	err         error

// 正在执行的分发操作数量
	activeDispatches int

// 收集已完成的分发
	completed chan *dispatchState

// 本次操作的模板类别
	variety tpb.TemplateVariety
}</pre>
<div class="blog_h3"><span class="graybg">TemplateInfo</span></div>
<p>和模板有关的信息：</p>
<pre class="crayon-plain-tag">type TemplateInfo struct {
// 模板名称
	Name             string
// 模板种类
	Variety          tpb.TemplateVariety
// 各种Mixer调用的函数指针
	DispatchReport   template.DispatchReportFn
	DispatchCheck    template.DispatchCheckFn
	DispatchQuota    template.DispatchQuotaFn
	DispatchGenAttrs template.DispatchGenerateAttributesFn
}</pre>
<div class="blog_h3"><span class="graybg">InstanceGroup </span></div>
<p>按照匹配条件分组的、需要发送给适配器的实例的信息：</p>
<pre class="crayon-plain-tag">type InstanceGroup struct {
	// 用于调试的ID
	id uint32

	// 预编译的表达式，何时应用此实例组
	Condition compiled.Expression

	// 用于构建出实例的函数+名称
	Builders []NamedBuilder

	// 映射器函数，用于将属性生成适配器的输出属性，映射入主属性集
	Mappers []template.OutputMapperFn
}

type NamedBuilder struct {
	InstanceShortName string
	Builder           template.InstanceBuilderFn
}

OutputMapperFn func(attrs attribute.Bag) (*attribute.MutableBag, error)</pre>
<div class="blog_h3"><span class="graybg">QuotaMethodArgs</span></div>
<p>进行配额请求时，需要的参数 + 配额（资源）的类型：</p>
<pre class="crayon-plain-tag">type QuotaMethodArgs struct {
	// 在出现RPC调用并重试时，用于额度分配/释放（Quota allocation/allocation）调用的去重复
	DeduplicationID string

	// 分配那种配额
	Quota string

	// 分配的量
	Amount int64

	// 如果设置为true，则允许响应返回比请求少的额度。如果设置为false，那么额度不足时，直接返回0
	BestEffort bool
}</pre>
<div class="blog_h3"><span class="graybg">QuotaArgs</span></div>
<p>进行配额请求时，需要的参数：</p>
<pre class="crayon-plain-tag">QuotaArgs struct {
	DeduplicationID string
	QuotaAmount int64
	BestEffort bool
}</pre>
<div class="blog_h3"><span class="graybg">QuotaResult</span></div>
<p>由处理器提供的，额度分配的结果： </p>
<pre class="crayon-plain-tag">QuotaResult struct {
	// RPC调用的状态（状态码、消息、详情）
	Status rpc.Status
	// 分配的额度何时过期，0表示永不过期
	ValidDuration time.Duration
	// 分配的额度，可能比请求的额度小
	Amount int64
}</pre>
<div class="blog_h1"><span class="graybg">Envoy代理请求过程</span></div>
<p>在探索Envoy如何向Mixer发送请求之前，我们先来分析一下Envoy作为网络代理，是如何工作的。</p>
<div class="blog_h2"><span class="graybg">整体过程</span></div>
<div class="blog_h3"><span class="graybg">启动监听</span></div>
<ol>
<li>通过xDS或者静态配置，获得Envoy代理的监听器信息</li>
<li>如果监听器bind_to_port，则直接调用libevent的接口，绑定监听，回调函数设置为ListenerImpl::listenCallback</li>
</ol>
<div class="blog_h3"><span class="graybg">连接接受</span></div>
<ol>
<li>DispatcherImpl通过libevent，接收到请求，调用ListenerImpl::listenCallback</li>
<li>根据入站时的目的端口，选择适当的监听器处理请求，调用onAccept。存在Iptables重定向的情况下，监听器为15001
<ol>
<li>构建出监听器过滤器链</li>
<li>执行过滤器链，对于15001来说，此链只有OriginalDstFilter一个过滤器</li>
<li>OriginalDstFilter恢复原始目的地址</li>
<li>查找和原始目的地址匹配的监听器，并<span style="background-color: #c0c0c0;">转交请求</span></li>
</ol>
</li>
<li>如果发生请求转交，则接受者监听器也会执行类似于2的逻辑。但是<span style="background-color: #c0c0c0;">不会再次发生转交</span></li>
<li>实际负责连接的那个监听器，会调用ActiveListener.newConnection，并间接的创建ConnectionImpl</li>
<li>ConnectionImpl会利用<span style="background-color: #c0c0c0;">连接套接字（ConnectionSocketPtr）的文件描述符，调用Dispatcher.createFileEvent，注册读写事件的回调</span></li>
<li>到此，连接接受完毕，后续的读写事件由libevent异步触发</li>
</ol>
<div class="blog_h3"><span class="graybg">数据读写</span></div>
<ol>
<li>发生可读、可写、关闭事件时，ConnectionImpl::onFileEvent被调用</li>
<li>可写事件的回调onWriteReady先调用</li>
<li>可读事件的回调onReadReady后调用
<ol>
<li>尝试循环读取，根据读取结果设置Post操作</li>
<li>处理读取到的数据
<ol>
<li>遍历网络过滤器链
<ol>
<li>如果是L7连接，则执行HTTP网络管理器</li>
</ol>
</li>
</ol>
</li>
<li>执行Post IO操作</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">L4核心接口</span></div>
<div class="blog_h3"><span class="graybg">LinkedObject</span></div>
<p>这个混入类为任意的unique_ptr所持有的对象增加行为，允许方便的将这种对象link/unlink到列表中：</p>
<pre class="crayon-plain-tag">template &lt;class T&gt; class LinkedObject {
public:
  // 对象唯一性指针的列表
  typedef std::list&lt;std::unique_ptr&lt;T&gt;&gt; ListType;

  // 返回列表的迭代器
  typename ListType::iterator entry();

  // 对象当前是否被插入到列表，只要调用过moveInto***方法就返回true
  bool inserted();

  // 在两个列表之间移动对象
  void moveBetweenLists(ListType&amp; list1, ListType&amp; list2);

  // 移动对象到列表，放在最前面，注意所有权的转移
  void moveIntoList(std::unique_ptr&lt;T&gt;&amp;&amp; item, ListType&amp; list);

  // 移动对象到列表，放在最后面
  void moveIntoListBack(std::unique_ptr&lt;T&gt;&amp;&amp; item, ListType&amp; list);

  // 从列表中移除条目
  std::unique_ptr&lt;T&gt; removeFromList(ListType&amp; list);
};</pre>
<div class="blog_h3"><span class="graybg">DeferredDeletable</span></div>
<p>标记性接口。任何实现此接口的对象，都可以传递给Dispatcher。Dispatcher确保，未来在事件循环中删除对象。</p>
<pre class="crayon-plain-tag">class DeferredDeletable {
public:
  virtual ~DeferredDeletable() {}
};</pre>
<p>使用此接口，进行事件处理时，不需要担心栈unwind的问题</p>
<div class="blog_h3"><span class="graybg">ConnectionHandler</span></div>
<p>抽象的连接处理器，总体负责网络连接的处理。<span style="background-color: #c0c0c0;">ActiveListener、ActiveSocket的_parent都指向此对象</span>。</p>
<pre class="crayon-plain-tag">class ConnectionHandler {
public:

  // 此处理器持有的活动连接数
  virtual uint64_t numConnections() PURE;

  // 添加一个监听器到此处理器
  virtual void addListener(ListenerConfig&amp; config) PURE;

  // 根据地址查找监听器。返回监听器的指针，所有权不转移
  virtual Network::Listener* findListenerByAddress(const Network::Address::Instance&amp; address) PURE;

  // 移除使用指定tag作为键的监听器。监听器拥有的所有连接也会被移除
  virtual void removeListeners(uint64_t listener_tag) PURE;

  // 停止使用指定tag作为键的监听器。监听器拥有的所有连接不会被关闭，此方法用于draining
  virtual void stopListeners(uint64_t listener_tag) PURE;

  // 停止所有监听器
  virtual void stopListeners() PURE;

  // 禁用所有监听器。不会关闭监听器拥有的连接鹅，用于临时暂停接受连接
  virtual void disableListeners() PURE;

  // 启用所有监听器
  virtual void enableListeners() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Listener</span></div>
<p>套接字监听器的抽象接口，是否此对象则停止对套接字的监听：</p>
<pre class="crayon-plain-tag">class Listener {
public:
  // 临时禁止接收新连接
  virtual void disable() PURE;

  // 继续接收新连接
  virtual void enable() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ActiveListener</span></div>
<p>表示某个连接处理器ConnectionHandler所拥有的活动的监听器， ActiveListener引用一个Listener。</p>
<div class="blog_h3"><span class="graybg">ListenerFilter</span></div>
<pre class="crayon-plain-tag">class ListenerFilter {
public:
  /**
   * 在新的连接被接受，但是Connection对象尚未创建之前调用
   * @param cb 此回调提供一些重要方法
   * @return 过滤器管理器根据此返回状态，决定是否继续迭代过滤器链
   */
  virtual FilterStatus onAccept(ListenerFilterCallbacks&amp; cb) PURE;
};</pre>
<p>通过参数cb，可以continueFilterChain。</p>
<div class="blog_h3"><span class="graybg">ListenerFilterManager</span></div>
<p>此接口用于管理监听器过滤器链：</p>
<pre class="crayon-plain-tag">class ListenerFilterManager {
public:
  // 为指定的监听器添加过滤器
  virtual void addAcceptFilter(ListenerFilterPtr&amp;&amp; filter) PURE;
};</pre>
<p>ActiveSocket实现了ListenerFilterManager、ListenerFilterCallbacks。</p>
<div class="blog_h3"><span class="graybg">ListenerFilterCallbacks</span></div>
<p>此接口供监听器过滤器使用，后者通过它和监听器管理器通信：</p>
<pre class="crayon-plain-tag">class ListenerFilterCallbacks {
public:
  /**
   * @return ConnectionSocket 过滤器所操作的连接套接字
   */
  virtual ConnectionSocket&amp; socket() PURE;

  /**
   * @return 分发事件的Dispatcher
   */
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  /**
   * 继续执行过滤器链
   */
  virtual void continueFilterChain(bool success) PURE;
};TransportSocket</pre>
<p>ActiveSocket实现了ListenerFilterManager、ListenerFilterCallbacks。 </p>
<div class="blog_h3"><span class="graybg">ListenerCallbacks</span></div>
<p>此接口供监听器使用：</p>
<pre class="crayon-plain-tag">class ListenerCallbacks {
public:
  /**
   * 当新连接被接受后，回调此方法
   * @param socket 移动到被调用者的套接字
   * @param redirected 提示套接字已经经过重定向
   */
  virtual void onAccept(ConnectionSocketPtr&amp;&amp; socket, bool hand_off_restored_destination_connections = true) PURE;

  /**
   * 当新连接被接受后，回调此方法
   * @param new_connection 移动到被调用者的套接字
   */
  virtual void onNewConnection(ConnectionPtr&amp;&amp; new_connection) PURE;
};</pre>
<p>ActiveListener实现了此接口。</p>
<div class="blog_h3"><span class="graybg">FilterChainManager</span></div>
<p>此接口用于管理过滤器链：</p>
<pre class="crayon-plain-tag">class FilterChainManager {
public:
  /**
   * 查找匹配新连接的元数据的过滤器链
   * @param socket 提供元数据
   * @return const FilterChain* 使用的过滤器链，如果没有匹配返回nullptr
   */
  virtual const FilterChain* findFilterChain(const ConnectionSocket&amp; socket) const PURE;
};</pre>
<p>ListenerImpl实现了此接口。</p>
<div class="blog_h3"><span class="graybg">FilterManager</span></div>
<p>负责添加网络过滤器给过滤器管理器，也就是Connection：</p>
<pre class="crayon-plain-tag">class FilterManager {
public:
  virtual ~FilterManager() {}

  // 添加一个写过滤器，过滤器以FIFO顺序调用
  virtual void addWriteFilter(WriteFilterSharedPtr filter) PURE;

  // 添加读写过滤器，相当于同时调用addWriteFilter/addReadFilter
  virtual void addFilter(FilterSharedPtr filter) PURE;

  // 添加一个读过滤器，过滤器以FIFO顺序调用
  virtual void addReadFilter(ReadFilterSharedPtr filter) PURE;

  // 实例化所有安装的读过滤器，相当于针对每个过滤器调用onNewConnection()
  virtual bool initializeReadFilters() PURE;
}</pre>
<div class="blog_h3"><span class="graybg">FilterChain</span></div>
<p>单个过滤器链的接口：</p>
<pre class="crayon-plain-tag">class FilterChain {
public:
  // 基于此过滤器链的新连接，使用的TransportSocketFactory，不同链使用的工厂可能不同（传输协议不同，RAW，TLS...）
  virtual const TransportSocketFactory&amp; transportSocketFactory() const PURE;
  // 基于此过滤器链的新连接，为了创建所有过滤器需要的工厂的集合
  virtual const std::vector&lt;FilterFactoryCb&gt;&amp; networkFilterFactories() const PURE;
}; </pre>
<div class="blog_h3"><span class="graybg">Filter</span></div>
<pre class="crayon-plain-tag">class Filter : public WriteFilter, public ReadFilter {};</pre>
<div class="blog_h3"><span class="graybg">ReadFilter</span></div>
<p>读处理路径（处理下游发来的数据）上的二进制（4层）过滤器：</p>
<pre class="crayon-plain-tag">class ReadFilter {
public:

  /**
   * 当连接上的数据被读取时调用
   * @param data 读取到的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 当新连接刚创建时调用，过滤器链的迭代可以被中止
   * @return status 过滤器管理器使用此状态决定如何进一步迭代其它过滤器
   */
  virtual FilterStatus onNewConnection() PURE;

  /**
   * 初始化用于和过滤器管理器交互的读过滤器回调，过滤器被注册时，将被过滤器管理器调用一次
   * 任何需要用到底层连接的构造，需要在此函数的回调中执行
   *
   * IMPORTANT: 出站、复杂逻辑不要在此，放在onNewConnection()
   *
   */
  virtual void initializeReadFilterCallbacks(ReadFilterCallbacks&amp; callbacks) PURE;
}</pre>
<div class="blog_h3"><span class="graybg">WriteFilter</span></div>
<p>写处理路径（向下游发送数据）上的二进制（4层）过滤器： </p>
<pre class="crayon-plain-tag">class WriteFilter {
public:
  /**
   * 当在此连接上发生数据写入时调用
   * @param data 需要写入的，可能已经被修改过的数据
   * @param end_stream 当连接启用半关闭语义时，用于提示是否到了最后一字节
   */
  virtual FilterStatus onWrite(Buffer::Instance&amp; data, bool end_stream) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ConnectionSocket</span></div>
<p>连接套接字，表示传递给一个Connection的套接字：</p>
<ol>
<li>对于服务端，该对象表示已经Accept的套接字</li>
<li>对于客户端，该对象表示正在连接到远程地址的套接字</li>
</ol>
<pre class="crayon-plain-tag">class ConnectionSocket : public virtual Socket {
public:
  // 返回远程地址
  virtual const Address::InstanceConstSharedPtr&amp; remoteAddress() const PURE;
  // 用于服务器端，恢复原始目的地址
  virtual void restoreLocalAddress(const Address::InstanceConstSharedPtr&amp; local_address) PURE;
  // 设置远程地址
  virtual void setRemoteAddress(const Address::InstanceConstSharedPtr&amp; remote_address) PURE;
  // 原始目的地址是否被恢复
  virtual bool localAddressRestored() const PURE;
  // 设置传输协议，例如RAW_BUFFER, TLS
  virtual void setDetectedTransportProtocol(absl::string_view protocol) PURE;
  // 返回传输协议
  virtual absl::string_view detectedTransportProtocol() const PURE;
  // 设置请求的应用协议，例如ALPN in TLS
  virtual void setRequestedApplicationProtocols(const std::vector&lt;absl::string_view&gt;&amp; protocol) PURE;
  // 返回请求的应用协议
  virtual const std::vector&lt;std::string&gt;&amp; requestedApplicationProtocols() const PURE;
  // 设置请求的服务器名称
  virtual void setRequestedServerName(absl::string_view server_name) PURE;
  // 返回请求的服务器名称
  virtual absl::string_view requestedServerName() const PURE;
};</pre>
<div class="blog_h3"><span class="graybg">TransportSocket</span></div>
<p>传输套接字，负责实际的读写，也进行某些数据转换（例如TLS）：</p>
<pre class="crayon-plain-tag">class TransportSocket {
public:
  // 连接对象调用此方法依次，初始化传输套接字的回调
  virtual void setTransportSocketCallbacks(TransportSocketCallbacks&amp; callbacks) PURE;

  // 由网络级协商选择的协议
  virtual std::string protocol() const PURE;

  // 套接字是否已经被flush和close
  virtual bool canFlushClose() PURE;

  // 关闭传输套接字
  virtual void closeSocket(Network::ConnectionEvent event) PURE;

  // 读取到缓冲
  virtual IoResult doRead(Buffer::Instance&amp; buffer) PURE;

  /**
   * 将缓冲写入底层套接字
   * @param buffer 缓冲
   * @param end_stream 提示是否是流的终点，如果true则缓冲中所有数据都被写出去，连接变成半关闭
   */
  virtual IoResult doWrite(Buffer::Instance&amp; buffer, bool end_stream) PURE;

  // 底层传输建立后回调
  virtual void onConnected() PURE;

  // 如果当前是SSL连接，则返回Ssl::Connection，否则返回nullptr
  virtual const Ssl::Connection* ssl() const PURE;
};</pre>
<div class="blog_h3"><span class="graybg">TransportSocketCallbacks</span></div>
<p>传输套接字使用此回调集，和Connection通信：</p>
<pre class="crayon-plain-tag">class TransportSocketCallbacks {
public:

  // 返回关联到连接的IO句柄，从此局部可以得到连接套接字的FD
  virtual IoHandle&amp; ioHandle() PURE;
  virtual const IoHandle&amp; ioHandle() const PURE;

  // 返回关联的连接
  virtual Network::Connection&amp; connection() PURE;

  // 是否读缓冲应该被排干（drain，也就是调用过滤器链进行处理），用于强制配置的读缓冲大小限制
  virtual bool shouldDrainReadBuffer() PURE;

  // 将读缓冲标记为可（被事件循环）读
  virtual void setReadBufferReady() PURE;

  // 发起（Raise）一个连接事件到Connection对象，TLS使用此方法告知握手完成
  virtual void raiseEvent(ConnectionEvent event) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">Connection</span></div>
<p>该接口表示原始的连接，它实现了FilterManager接口：</p>
<pre class="crayon-plain-tag">class Connection : public Event::DeferredDeletable, public FilterManager {
public:
  // 状态枚举
  enum class State { Open, Closing, Closed };

  // 连接发送字节后的回调
  typedef std::function&lt;void(uint64_t bytes_sent)&gt; BytesSentCb;

  // 注册当此连接上发生事件后执行的回调
  virtual void addConnectionCallbacks(ConnectionCallbacks&amp; cb) PURE;

  // 注册每当bytes被写入底层TransportSocket后执行的回调
  virtual void addBytesSentCallback(BytesSentCb cb) PURE;

  // 为此连接启用半关闭语义，从一个已经被对端半关闭的连接上进行读操作，不会关闭连接
  virtual void enableHalfClose(bool enabled) PURE;

  // 关闭连接
  virtual void close(ConnectionCloseType type) PURE;

  // 返回分发器
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  // 返回唯一性的本地连接ID
  virtual uint64_t id() const PURE;

  // 返回网络级协商选择的下一个使用的协议
  virtual std::string nextProtocol() const PURE;

  // 为连接启用/禁用NO_DELAY
  virtual void noDelay(bool enable) PURE;

  // 启禁针对此连接的套接字读。当重新启用读时，如果输入缓冲有内容，会通过过滤器链分发
  virtual void readDisable(bool disable) PURE;

  // 当禁用套接字读后，Envoy是否应当检测TCP连接关闭。默认对新连接来说，检测
  virtual void detectEarlyCloseWhenReadDisabled(bool should_detect) PURE;

  // 读操作是否启用
  virtual bool readEnabled() const PURE;

  // 返回远程地址
  virtual const Network::Address::InstanceConstSharedPtr&amp; remoteAddress() const PURE;

  // 返回本地地址，对于客户端连接来说，即原始地址；对于服务器连接来说
  // 是本地的目的地址
  // 对于服务器连接来说，此地址可能和代理的监听地址不一样，因为下游连接可能被重定向，或者代理在透明模式下运行
  virtual const Network::Address::InstanceConstSharedPtr&amp; localAddress() const PURE;

  // 更新连接状态，出于性能的考虑，最终一致
  virtual void setConnectionStats(const ConnectionStats&amp; stats) PURE;

  // 如果该连接是SSL，则返回SSL连接数据；否则返回nullptr
  virtual const Ssl::Connection* ssl() const PURE;

  // 返回服务器名称，对于TLS来说即SNI
  virtual absl::string_view requestedServerName() const PURE;

  // 返回连接状态
  virtual State state() const PURE;

  /**
   * 写入数据到连接，数据将经过过滤器链
   * @param data 需要写入的数据
   * @param end_stream 如果为true，则提示此为最后一次写操作，导致连接半关闭。必须enableHalfClose(true)才能传入true
   */
  virtual void write(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 设置该连接的缓冲区的软限制
  // 对于读缓冲，限制处理流水线在flush到下一个stage前能缓冲的最大字节数
  // 对于写缓冲，设置水位。如果缓冲了足够的数据，触发onAboveWriteBufferHighWatermark调用
  virtual void setBufferLimits(uint32_t limit) PURE;

  // 获得软限制
  virtual uint32_t bufferLimit() const PURE;

  // 本地地址是否被还原为原始目的地址
  virtual bool localAddressRestored() const PURE;

  // 连接当前是否高于高水位
  virtual bool aboveHighWatermark() const PURE;

  // 获取此连接的套接字选项
  virtual const ConnectionSocket::OptionsSharedPtr&amp; socketOptions() const PURE;

  // 获取关联到此连接的StreamInfo对象。StreamInfo典型用于日志目的
  // 每个过滤器都可以通过StreamInfo.FilterState来添加特定的信息
  // 在此上下文中每个连接对应一个StreamInfo。而对于HTTP连接管理器，每个请求对应一个StreamInfo
  virtual StreamInfo::StreamInfo&amp; streamInfo() PURE;
  virtual const StreamInfo::StreamInfo&amp; streamInfo() const PURE;

  // 设置延迟连接关闭的超时
  virtual void setDelayedCloseTimeout(std::chrono::milliseconds timeout) PURE;
  virtual std::chrono::milliseconds delayedCloseTimeout() const PURE;
}</pre>
<div class="blog_h3"><span class="graybg">ConnectionCallbacks</span></div>
<p>L4连接上发生的事件的回调：</p>
<pre class="crayon-plain-tag">class ConnectionCallbacks {
public:
  virtual ~ConnectionCallbacks() {}

  // ConnectionEvent的回调
  virtual void onEvent(ConnectionEvent event) PURE;

  // 当连接的写缓冲超过高水位后调用
  virtual void onAboveWriteBufferHighWatermark() PURE;

  // 当连接的写缓冲，从超过高水位变为低于低水位后调用
  virtual void onBelowWriteBufferLowWatermark() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ActiveConnection</span></div>
<p>表示某个连接处理器所（通过ActiveListener）拥有的活动的连接。ActiveConnection引用一个Connection、一个ActiveListener。</p>
<div class="blog_h2"><span class="graybg">L7核心接口</span></div>
<div class="blog_h3"><span class="graybg">Connection</span></div>
<p>表示可以拥有多个流（Stream）的HTTP客户端/服务器连接：</p>
<pre class="crayon-plain-tag">class Connection {
public:

  // 分发入站的请求数据
  virtual void dispatch(Buffer::Instance&amp; data) PURE;

  // 给对端提示以go away，从此时开始，不能创建新的流
  virtual void goAway() PURE;

  // 返回连接的协议
  virtual Protocol protocol() PURE;

  // 给对端提示以shutdown notice，对端不应该在发送任何新的流，但是对于已经达到的流u，不会被重置
  virtual void shutdownNotice() PURE;

  // HTTP编解码器是否有数据需要写入，但是由于协议的原因（例如窗口更新），无法完成
  virtual bool wantsToWrite() PURE;

  // 当底层的Network::Connection超过高水位后，调用此方法
  virtual void onUnderlyingConnectionAboveWriteBufferHighWatermark() PURE;

  // 当底层的Network::Connection超过高水位后，然后由低于低水位后调用此方法
  virtual void onUnderlyingConnectionBelowWriteBufferLowWatermark() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ConnectionCallbacks</span></div>
<p>HTTP连接级别的回调：</p>
<pre class="crayon-plain-tag">class ConnectionCallbacks {
public:
  // 对端提示go away时触发此回调，不允许创建新流
  virtual void onGoAway() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">ServerConnection</span></div>
<p>服务器端连接：</p>
<pre class="crayon-plain-tag">class ServerConnection : public virtual Connection {};</pre>
<p>HTTP连接管理器ConnectionManagerImpl.codec字段是ServerConnection类型，后者承担读取到的请求数据的分发（Dispatch，给HTTP解析器）职责。</p>
<div class="blog_h3"><span class="graybg">ServerConnectionCallbacks</span></div>
<p>继承ConnectionCallbacks回调，并添加方法：</p>
<pre class="crayon-plain-tag">class ServerConnectionCallbacks : public virtual ConnectionCallbacks {
public:
  /**
   * 当对端初始化一个新的请求流后触发此回调
   * @param response_encoder 提供用于创建响应的编码器，请求、响应由同一流对象管理
   * @param is_internally_created 提示此流是流客户端创建，还是由Envoy自己创建（例如内部重定向）
   */
  virtual StreamDecoder&amp; newStream(StreamEncoder&amp; response_encoder,
                                   bool is_internally_created = false) PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoder</span></div>
<p>HTTP流解码器，可以解码下游发来的请求：</p>
<pre class="crayon-plain-tag">class StreamDecoder {
public:
  virtual ~StreamDecoder() {}

  // 处理解码后的100-Continue头的map
  virtual void decode100ContinueHeaders(HeaderMapPtr&amp;&amp; headers) PURE;

  // 处理解码后的头
  virtual void decodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) PURE;

  // 处理解码后的数据帧
  virtual void decodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 处理解码后的尾帧
  virtual void decodeTrailers(HeaderMapPtr&amp;&amp; trailers) PURE;

  // 处理解码后的元数据
  virtual void decodeMetadata(MetadataMapPtr&amp;&amp; metadata_map) PURE;
};</pre>
<p>这里这里的decode有歧义：</p>
<ol>
<li>decoded，表示经由http_parser解析，结构化为C++对象 —— HTTP语境</li>
<li>decode，调用Envoy的流解码器处理那些C++对象 —— Envoy语境</li>
</ol>
<div class="blog_h3"><span class="graybg">StreamEncoder</span></div>
<p>HTTP流编码器，可以编码需要发送给下游的应答，接口类似于StreamDecoder。</p>
<div class="blog_h3"><span class="graybg">StreamCallbacks</span></div>
<p>针对HTTP流的回调：</p>
<pre class="crayon-plain-tag">class StreamCallbacks {
public:
  // 对端重置了流后调用，参数是重置原因
  virtual void onResetStream(StreamResetReason reason) PURE;

  // 当一个流（HTTP2），或者流发向的连接（HTTP1）超过高水位后调用
  virtual void onAboveWriteBufferHighWatermark() PURE;

  // 当一个流（HTTP2），或者流发向的连接（HTTP1）从超过高水位降到低于低水位后调用
  virtual void onBelowWriteBufferLowWatermark() PURE;
} </pre>
<div class="blog_h3"><span class="graybg">ActiveStream</span></div>
<p>表示连接上的单个HTTP流，实现了StreamDecoder、StreamCallbacks、FilterChainFactoryCallbacks接口。</p>
<div class="blog_h3"><span class="graybg">FilterChainFactoryCallbacks</span></div>
<p>HTTP连接管理器提供给过滤器链工厂的回调集，依赖于此回调工厂能够以应用程序特定的方式构建过滤器链：</p>
<pre class="crayon-plain-tag">class FilterChainFactoryCallbacks {
public:

  // 添加读取流数据时使用的解码器
  virtual void addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr filter) PURE;

  // 添加写入流数据时使用的编码器
  virtual void addStreamEncoderFilter(Http::StreamEncoderFilterSharedPtr filter) PURE;

  // 添加读写编解码器
  virtual void addStreamFilter(Http::StreamFilterSharedPtr filter) PURE;

  // 添加访问日志处理器，在流被销毁时调用
  virtual void addAccessLogHandler(AccessLog::InstanceSharedPtr handler) PURE;
}</pre>
<div class="blog_h3"><span class="graybg">StreamFilterBase</span></div>
<p>流编码/解码过滤器的基类：</p>
<pre class="crayon-plain-tag">class StreamFilterBase {
public:
  /**
   * 在过滤器被销毁前调用此方法，这可能发生在
   * 1、正常的流（下游+上游）完成后
   * 2、发生重置后
   * 每个过滤器负责确保在此方法的上下文中，所有异步事件被清理完毕。这些异步事件包括定时器、网络调用等
   *
   * 不在析构函数中进行清理而使用onDestroy钩子的原因和Envoy的延迟删除模型有关。此模型规避了Stack unwind
   * 有关的复杂性。在onDestroy之后，过滤器不得调用编码/解码过滤器回调
   */
  virtual void onDestroy() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoderFilter</span></div>
<p>流解码过滤器，继承StreamFilterBase：</p>
<pre class="crayon-plain-tag">class StreamDecoderFilter : public StreamFilterBase {
public:
  // 解码请求头
  virtual FilterHeadersStatus decodeHeaders(HeaderMap&amp; headers, bool end_stream) PURE;

  // 解码数据帧
  virtual FilterDataStatus decodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  // 解码请求尾
  virtual FilterTrailersStatus decodeTrailers(HeaderMap&amp; trailers) PURE;

  // 设置此解码过滤器的过滤器回调
  virtual void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&amp; callbacks) PURE;
} </pre>
<div class="blog_h3"><span class="graybg">StreamFilterCallbacks</span></div>
<p>传递给所有（读/写）过滤器的回调函数集，用于写响应数据、和底层流交互：</p>
<pre class="crayon-plain-tag">class StreamFilterCallbacks {
public:

  // 获取L4网络连接
  virtual const Network::Connection* connection() PURE;

  // 返回线程本地的Dispatcher，从此分发器来分配定时器
  virtual Event::Dispatcher&amp; dispatcher() PURE;

  // 将底层的流进行重置
  virtual void resetStream() PURE;

  // 返回当前请求使用的路由
  // 实现应当能够进行路由缓存，避免反复查找。如果过滤器修改了请求头，则路由可能需要改变，此时应当调用clearRouteCache()

  // 未来可能会允许过滤器对路由条目进行覆盖
  virtual Router::RouteConstSharedPtr route() PURE;

  // 返回被缓存的路由条目的上游集群信息（clusterInfo）。该方法用于避免在过滤器链中进行反复的查找，同时
  // 确保当路由被picked/repicked后能提供clusterInfo的一致性视图
  virtual Upstream::ClusterInfoConstSharedPtr clusterInfo() PURE;

  // 为当前请求清除路由缓存，如果过滤器修改了请求头，并且此修改可能影响选路，则必须调用该方法
  virtual void clearRouteCache() PURE;

  // 返回用于日志目的的流唯一标识
  virtual uint64_t streamId() PURE;

  // 返回用于日志目的的StreamInfo
  virtual StreamInfo::StreamInfo&amp; streamInfo() PURE;

  // 返回追踪用的当前追踪上下文
  virtual Tracing::Span&amp; activeSpan() PURE;

  // 返回追踪配置
  virtual const Tracing::Config&amp; tracingConfig() PURE;
};</pre>
<div class="blog_h3"><span class="graybg">StreamDecoderFilterCallbacks</span></div>
<p>继承StreamFilterCallbacks，添加专用于解码（读）过滤器的回调：</p>
<pre class="crayon-plain-tag">class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks {
public:
  /**
   * 使用缓冲的头，以及请求体，继续迭代过滤器链。该方法仅仅在以下情况之一才会调用
   * 1、先前的过滤器在decodeHeaders()后返回StopIteration
   * 2、先前的过滤器在decodeData()后返回StopIterationAndBuffer, StopIterationAndWatermark 或 StopIterationNoBuffer
   *
   * HTTP连接管理器会分发请求头、缓冲的请求体给过滤器链中的下一个过滤器
   * 
   * 如果请求没有完成，当前过滤器仍然会继续接受decodeData()调用，并且必须返回适当的的状态码
   *
   */
  virtual void continueDecoding() PURE;

  // 返回当前过滤器、或者链中先前过滤器缓冲的数据。如果尚未缓冲任何内容，返回nullpt
  virtual const Buffer::Instance* decodingBuffer() PURE;

  /**
   * 添加解码处理后的、缓冲的请求体数据。在某些高级用例中，decodeData()返回StopIterationAndBuffer不能满足
   * 需要，需要调用此方法：
   *
   * 1) 对于header-only请求需要被转换为包含请求体的请求，可以在 decodeHeaders() 回调中调用此方法，添加请求体
   * 后续过滤器会依次接收调用decodeHeaders(..., false)、decodeData(..., true)。在直接迭代、停止后继续迭代
   * 场景下，都可以使用
   * 
   *
   * 2) 如果某个过滤器希望在end_stream=true的情况下，在一个数据回调中查看所有缓冲的数据，可以调用该方法，以立即缓冲数据
   * 避免同时处理已缓冲数据、以及当前回调产生的数据
   *
   * 3) 如果某个过滤器在调用后续过滤器时，需要添加额外的缓冲请求体数据
   *
   * 4) 如果在decodeTrailers()回调中需要添加额外的数据。可以在前述回调的上下文中调用此方法
   * 所有后续过滤器会依次接受decodeData(..., false)、decodeTrailers()调用
   *
   * 在其它场景下调用此方法是错误
   *
   * @param data Buffer::Instance 添加需要被解码的数据
   * @param streaming_filter boolean 提示该过滤器是流式处理还是缓冲了完整请求体
   */
  virtual void addDecodedData(Buffer::Instance&amp; data, bool streaming_filter) PURE;

  /**
   * 添加解码后的请求尾。只能在end_stream=true时在decodeData中调用 
   * 在decodeData中调用时，请求尾映射被初始化为空map并以引用的方式返回
   * 该方法最多调用一次
   *
   * @return 返回新的空请求尾map
   */
  virtual HeaderMap&amp; addDecodedTrailers() PURE;

  /* 基于其它的状态码、请求体，生成一个Envoy本地的响应并发送给下游
   * 如果是gRPC请求，则本地响应编码为gRPC响应，HTTP状态码置为200。从参数生成grpc-status、grpc-message
   *
   * @param response_code HTTP状态码
   * @param body_text HTTP请求体，以text/plain发送或者编码在grpc-message头中
   * @param modify_headers 可选的回调函数，用于修改响应头
   * @param grpc_status gRPC状态码，覆盖通过httpToGrpcStatus推导出的gRPC状态码
   */
  virtual void sendLocalReply(Code response_code, absl::string_view body_text,
                              std::function&lt;void(HeaderMap&amp; headers)&gt; modify_headers,
                              const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status) PURE;

  /**
   * 编码100-Continue响应头。该头不在encodeHeaders中处理，因为大部分情况下Envoy用户和过滤器
   * 不希望代理100-Continue，而是直接吐出，可以忽略多次编码响应头encodeHeaders()的复杂性
   *
   * @param headers supplies 需要编码的头
   */
  virtual void encode100ContinueHeaders(HeaderMapPtr&amp;&amp; headers) PURE;

  /**
   * 编码响应头。HTTP连接管理器会自动探测一些不发给下游的伪头
   *
   * @param headers 需要编码的头
   * @param end_stream 这是不是一个header-only的request/response
   */
  virtual void encodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) PURE;

  /**
   * 编码响应数据
   * @param data 需要编码的数据
   * @param end_stream 提示这是不是最后一个数据帧
   */
  virtual void encodeData(Buffer::Instance&amp; data, bool end_stream) PURE;

  /**
   * 编码响应尾数据，隐含意味着流的结束
   * @param trailers supplies 需要编码的尾
   */
  virtual void encodeTrailers(HeaderMapPtr&amp;&amp; trailers) PURE;

  /**
   * 编码元数据
   *
   * @param metadata_map supplies 需要编码的元数据的unique_ptr
   */
  virtual void encodeMetadata(MetadataMapPtr&amp;&amp; metadata_map) PURE;

  /**
   * 当解码过滤器的缓冲，或者过滤器需要发送数据到的那些缓冲，超越高水位后调用
   *
   * 对于路由过滤器这样的HTTP过滤器，会使用多个缓冲（codec、connection...），该方法可能被调用多次
   * 这些过滤器应当负责，在对应的缓冲被排干后，以等同次数调用低水位回调connection etc.)
   */
  virtual void onDecoderFilterAboveWriteBufferHighWatermark() PURE;

  /**
   * 当解码过滤器的缓冲，或者过滤器需要发送数据到的那些缓冲，超越高水位后，后降低到低于低水位后调用
   */
  virtual void onDecoderFilterBelowWriteBufferLowWatermark() PURE;

  /**
   * 需要订阅下游流、下游连接上的水位事件的过滤器，调用此方法
   * 订阅后，对于每个outstanding backed up buffer，过滤器的回调都被调用
   */
  virtual void addDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks&amp; callbacks) PURE;

  /**
   * 需要停止订阅下游流、下游连接上的水位事件的过滤器，调用此方法
   * 在DownstreamWatermarkCallbacks的回调函数的栈下面调用此方法不安全
   */
  virtual void removeDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks&amp; callbacks) PURE;

  /**
   * 用于改变解码过滤器的缓冲区大小
   *
   * @param limit 新的缓冲大小
   */
  virtual void setDecoderBufferLimit(uint32_t limit) PURE;

  // 返回解码过滤器缓冲大小，0表示无限制u
  virtual uint32_t decoderBufferLimit() PURE;

  // 将当前流看作是新的，就好像它的所有头都是刚到达一样
  // 如果操作成功，会导致创建新的过滤器链，并且上游请求可能和原始的下游流关联
  // 如果操作失败，并且下面列出的前置条件不满足，则调用者负责处理和终止原始流
  //
  // 前置条件
  //   - 流必须已经被完整的读取
  //   - 流必须没有请求体
  //
  // 注意HTTP消毒器不会针对这种重新创建的流进行操作，它假设消毒已经完成
  virtual bool recreateStream() PURE;
} </pre>
<div class="blog_h3"><span class="graybg">ActiveStreamDecoderFilter</span></div>
<p>表示活动的解码过滤器，继承ActiveStreamFilterBase，实现了StreamFilterCallbacks、StreamDecoderFilterCallbacks，也就是说，过滤器和过滤器回调是一体的。</p>
<p>该对象持有一个StreamDecoderFilter对象。ActiveStream通过字段<pre class="crayon-plain-tag">std::list&lt;ActiveStreamDecoderFilterPtr&gt; decoder_filters_</pre>来引用这种对象。</p>
<div class="blog_h2"><span class="graybg">HTTP1核心接口</span></div>
<div class="blog_h3"><span class="graybg">ActiveRequest</span></div>
<p>多个地方存在命名为ActiveRequest的结构，表示一个活动的HTTP1请求。</p>
<p>例如，作为Http::Http1::ServerConnectionImpl的私有成员：</p>
<pre class="crayon-plain-tag">struct ActiveRequest {
  // 构造请求对象时，必须传入响应编码器
  ActiveRequest(ConnectionImpl&amp; connection) : response_encoder_(connection) {}

  HeaderString request_url_;
  // 请求解码器
  StreamDecoder* request_decoder_{};
  // 响应编码器
  ResponseStreamEncoderImpl response_encoder_;
  // 请求的处理是否已经完毕
  bool remote_complete_{};
};</pre>
<p>作为Http::CodecClient的私有成员：</p>
<pre class="crayon-plain-tag">struct ActiveRequest : LinkedObject&lt;ActiveRequest&gt;,
                       public Event::DeferredDeletable,
                       public StreamCallbacks,
                       public StreamDecoderWrapper {
  ActiveRequest(CodecClient&amp; parent, StreamDecoder&amp; inner) : StreamDecoderWrapper(inner), parent_(parent) {}

  // 流回调函数
  void onResetStream(StreamResetReason reason) override { parent_.onReset(*this, reason); }
  void onAboveWriteBufferHighWatermark() override {}
  void onBelowWriteBufferLowWatermark() override {}
  void onPreDecodeComplete() override { parent_.responseDecodeComplete(*this); }
  void onDecodeComplete() override {}

  // 编码器
  StreamEncoder* encoder_{};
  CodecClient&amp; parent_;
} </pre>
<div class="blog_h2"><span class="graybg">启动监听</span></div>
<p>Envoy代理的需要创建哪些监听器，由Bootstrap配置 + xDS响应共同决定，本文不讨论细节，可以参考<a href="/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>。</p>
<p>如果某个监听器配置了bind_to_port（默认情况下virtual 15001配置了），则会调用libevent的API，注册套接字监听事件：</p>
<pre class="crayon-plain-tag">ListenerImpl::ListenerImpl(Event::DispatcherImpl&amp; dispatcher, Socket&amp; socket, ListenerCallbacks&amp; cb,
                           bool bind_to_port, bool hand_off_restored_destination_connections)
    : BaseListenerImpl(dispatcher, socket), cb_(cb),
      hand_off_restored_destination_connections_(hand_off_restored_destination_connections),
      listener_(nullptr) {
  if (bind_to_port) {
    // 注册监听
    setupServerSocket(dispatcher, socket);
  }
}

void ListenerImpl::setupServerSocket(Event::DispatcherImpl&amp; dispatcher, Socket&amp; socket) {
  // 重置 CSmartPtr&lt;evconnlistener, evconnlistener_free&gt; ListenerPtr引用为新的evconnlistener
  listener_.reset(
      // libevent的base                      当前对象方法                   套接字的文件描述符
      evconnlistener_new(&amp;dispatcher.base(), listenCallback, this, 0, -1, socket.ioHandle().fd()));

  if (!listener_) {
    throw CreateListenerException(
        fmt::format("cannot listen on socket: {}", socket.localAddress()-&gt;asString()));
  }

  if (!Network::Socket::applyOptions(socket.options(), socket,
                                     envoy::api::v2::core::SocketOption::STATE_LISTENING)) {
    throw CreateListenerException(fmt::format("cannot set post-listen socket option on socket: {}",
                                              socket.localAddress()-&gt;asString()));
  }

  evconnlistener_set_error_cb(listener_.get(), errorCallback);
}</pre>
<div class="blog_h2"><span class="graybg">接受连接</span></div>
<p>Envoy默认会在15001端口上监听，当流量到达（通常是通过其它端口重定向到15001）时，Envoy的DispatcherImpl循环得到通知，并通过libevent回调Envoy::Network::ListenerImpl::listenCallback方法，该方法是一切<span style="background-color: #c0c0c0;">新连接处理的起点</span>：</p>
<pre class="crayon-plain-tag">void ListenerImpl::listenCallback(evconnlistener*, evutil_socket_t fd, sockaddr* remote_addr,
                                  int remote_addr_len, void* arg) {
  // 传递的参数是监听器对象
  ListenerImpl* listener = static_cast&lt;ListenerImpl*&gt;(arg);

  // IoSocketHandle为IO操作的抽象接口
  IoHandlePtr io_handle = std::make_unique&lt;IoSocketHandle&gt;(fd);

  // 如果监听器在ANY地址（0.0.0.0）上监听，从新套接字上获取本地地址
  const Address::InstanceConstSharedPtr&amp; local_address =
  // 获取远程地址
  const Address::InstanceConstSharedPtr&amp; remote_address =
      (remote_addr-&gt;sa_family == AF_UNIX)
          ? Address::peerAddressFromFd(io_handle-&gt;fd())
          : Address::addressFromSockAddr(*reinterpret_cast&lt;const sockaddr_storage*&gt;(remote_addr),
                                         remote_addr_len,
                                         local_address-&gt;ip()-&gt;version() == Address::IpVersion::v6);
  // 调用监听器的onAccept回调
  listener-&gt;cb_.onAccept(
      std::make_unique&lt;AcceptedSocketImpl&gt;(std::move(io_handle), local_address, remote_address),
      listener-&gt;hand_off_restored_destination_connections_);
}</pre>
<p>回调ConnectionHandlerImpl::ActiveListener::onAccept的逻辑如下：</p>
<pre class="crayon-plain-tag">// 此回调在新连接创建后调用
// socket 需要移动给被调用者的套接字对象
// redirected 如果套接字是第一次被其它监听器接受，并且随后被重定向给一个新的监听器时，为true
//            接收者监听器不得再次重定向
void ConnectionHandlerImpl::ActiveListener::onAccept(
    Network::ConnectionSocketPtr&amp;&amp; socket, bool hand_off_restored_destination_connections) {
  // 代表当前正在处理的套接字对象
  auto active_socket = std::make_unique&lt;ActiveSocket&gt;(*this, std::move(socket),
                                                      hand_off_restored_destination_connections);

  // 构建出过监听器过滤器链
  config_.filterChainFactory().createListenerFilterChain(*active_socket);
  // 开始迭代过滤器链
  active_socket-&gt;continueFilterChain(true);

  // 如果监听器过滤器链没有迭代完毕，则active_socket暂存到sockets_列表里
  // 防止active_socket因为超出作用域而被自动删除
  if (active_socket-&gt;iter_ != active_socket-&gt;accept_filters_.end()) {
    active_socket-&gt;startTimer();
    active_socket-&gt;moveIntoListBack(std::move(active_socket), sockets_);
  }
}</pre>
<div class="blog_h3"><span class="graybg">监听器过滤器链</span></div>
<p>ActiveSocket实现了Network::ListenerFilterManager：</p>
<pre class="crayon-plain-tag">class ListenerFilterManager {
public:
  virtual ~ListenerFilterManager() {}
  // 为监听器添加一个过滤器，过滤器以FIFO顺序被调用
  virtual void addAcceptFilter(ListenerFilterPtr&amp;&amp; filter) PURE;
};</pre>
<p>ListenerImpl的createListenerFilterChain方法支持为ListenerFilterManager提供过滤器链：</p>
<pre class="crayon-plain-tag">bool ListenerImpl::createListenerFilterChain(Network::ListenerFilterManager&amp; manager) {
  return Configuration::FilterChainUtility::buildFilterChain(manager, listener_filter_factories_);
}</pre>
<p>listener_filter_factories_在ListenerImpl初始化阶段创建，它是Network::ListenerFilterFactoryCb的迭代器，表示<span style="background-color: #c0c0c0;">当前监听器启用的所有监听器过滤器的工厂回调的集合</span>。调用ListenerFilterFactoryCb可以将过滤器安装到ListenerFilterManager，也就是ActiveSocket上：</p>
<pre class="crayon-plain-tag">bool FilterChainUtility::buildFilterChain( Network::ListenerFilterManager&amp; filter_manager,
    const std::vector&lt;Network::ListenerFilterFactoryCb&gt;&amp; factories) {
  for (const Network::ListenerFilterFactoryCb&amp; factory : factories) {
    factory(filter_manager);
  }
  return true;
}</pre>
<p>默认情况下，监听器virtual（15001端口）只配置一个监听器过滤器OriginalDstFilter，它的工厂如下：</p>
<pre class="crayon-plain-tag">class OriginalDstConfigFactory : public Server::Configuration::NamedListenerFilterConfigFactory {
public:
  // 此即工厂回调的工厂
  Network::ListenerFilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message&amp;,
                               Server::Configuration::ListenerFactoryContext&amp;) override {
    return [](Network::ListenerFilterManager&amp; filter_manager) -&gt; void {
      // 上段代码的factory(filter_manager)调用的是此Lambda
      // 简单的创建OriginalDstFilter并添加到管理器
      filter_manager.addAcceptFilter(std::make_unique&lt;OriginalDstFilter&gt;());
    };
  }

  ProtobufTypes::MessagePtr createEmptyConfigProto() override {
    return std::make_unique&lt;Envoy::ProtobufWkt::Empty&gt;();
  }

  std::string name() override { return ListenerFilterNames::get().OriginalDst; }
};</pre>
<div class="blog_h3"><span class="graybg">迭代监听器过滤器链</span></div>
<p>当一个过滤器返回FilterStatus::StopIteration以终止过滤器迭代，那么：</p>
<ol>
<li>如果希望继续遍历后续过滤器链，以true参数调用下面的方法</li>
<li>如果过滤器执行失败，需要关闭连接，以false参数调用下面的方法</li>
</ol>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveSocket::continueFilterChain(bool success) {
  // 开始/继续迭代
  if (success) {
    if (iter_ == accept_filters_.end()) {
      iter_ = accept_filters_.begin();
    } else {
      iter_ = std::next(iter_);
    }
    // 从当前过滤器迭代到监听器过滤器集的尾部
    for (; iter_ != accept_filters_.end(); iter_++) {
     // 调用监听器过滤器的onAccept方法，this就是ActievSocket对象
      Network::FilterStatus status = (*iter_)-&gt;onAccept(*this);
      // 上一个过滤器提示，应当停止迭代
      if (status == Network::FilterStatus::StopIteration) {
        // 则本次过滤器迭代终止。上一个过滤器负责在未来重启迭代
        return;
      }
    }
    // 所有监听器过滤器都执行成功

    // 检查套接字是否需要重定向给其它监听器
    ActiveListener* new_listener = nullptr;

    // OriginalDstFilter会导致下面的分支执行
    if (hand_off_restored_destination_connections_ &amp;&amp; socket_-&gt;localAddressRestored()) {
      // 找到匹配原始目的地址的那个监听器
      new_listener = listener_.parent_.findActiveListenerByAddress(*socket_-&gt;localAddress());
    }
    if (new_listener != nullptr) {
      // 将由Iptables重定向到当前监听器的连接转交给匹配原始目的地址的监听器处理
      // 同时传递hand_off_restored_destination_connections=false，防止再次重定向
      new_listener-&gt;onAccept(std::move(socket_), false);
    } else {
      // 非重定向连接，或者重定向连接的接收者监听器
      if (socket_-&gt;detectedTransportProtocol().empty()) {
        // 设置默认传输协议
        socket_-&gt;setDetectedTransportProtocol(
            Extensions::TransportSockets::TransportSocketNames::get().RawBuffer);
      }
      // 在此监听器上创建新的连接对象
      listener_.newConnection(std::move(socket_));
    }
  }

  // 过滤器执行完毕，如果ActiveSocket已经linked，则unlink并删除
  if (inserted()) {
    unlink();
  }
}</pre>
<div class="blog_h3"><span class="graybg">OriginalDstFilter</span></div>
<p>该监听器过滤器的onAccept方法的实现如下：</p>
<pre class="crayon-plain-tag">Network::FilterStatus OriginalDstFilter::onAccept(Network::ListenerFilterCallbacks&amp; cb) {
  ENVOY_LOG(debug, "original_dst: New connection accepted");
  Network::ConnectionSocket&amp; socket = cb.socket();
  const Network::Address::Instance&amp; local_address = *socket.localAddress();

  // 通过系统调用os_syscalls.getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, &amp;orig_addr, &amp;addr_len)获取原始目的IP
  if (local_address.type() == Network::Address::Type::Ip) {
    // 我们的例子中，原始目的IP地址为127.0.0.1:9898
    Network::Address::InstanceConstSharedPtr original_local_address = getOriginalDst(socket.ioHandle().fd());

    // 即使对于use_original_dst设置为true的监听器（也就是当前监听器），仍然能够接收不是由iptables重定向的连接
    // 如果连接不是被重定向的，那么getOriginalDst()的返回值和当前套接字的本地地址（Envoy代理端）相同
    // 这种情况下，当前监听器直接处理连接，而不会转交（hand off）给其它监听器
    if (original_local_address) {
      // 修改local_address_，并设置local_address_restored_为true
      socket.restoreLocalAddress(original_local_address);
    }
  }
  // 总是继续迭代监听器过滤器链
  return Network::FilterStatus::Continue;
}</pre>
<div class="blog_h2"><span class="graybg">处理TCP连接</span></div>
<p>监听器过滤器的处理是以ActiveSocket为中心的，套接字请求接受后，连接的处理则以ActiveListener为中心。 </p>
<p>执行完监听器过滤器后，ActiveSocket调用ActiveListener.newConnection，开始连接的处理：</p>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveListener::newConnection(Network::ConnectionSocketPtr&amp;&amp; socket) {
  // 首先，查找匹配此套接字的过滤器链
  const auto filter_chain = config_.filterChainManager().findFilterChain(*socket);
  if (filter_chain == nullptr) {
    // 找不到过滤器，记录统计信息，关闭套接字，结束处理
    ENVOY_LOG_TO_LOGGER(parent_.logger_, debug, "closing connection: no matching filter chain found");
    stats_.no_filter_chain_match_.inc();
    socket-&gt;close();
    return;
  }
  // 创建一个传输套接字，此套接字负责实际的读写操作
  // 具体工厂和协议有关，默认RawBufferSocketFactory，创建RawBufferSocket
  auto transport_socket = filter_chain-&gt;transportSocketFactory().createTransportSocket(nullptr);
  // 创建连接对象，设置为connected
  Network::ConnectionPtr new_connection = parent_.dispatcher_.createServerConnection(std::move(socket), std::move(transport_socket));
  // 设置缓冲区大小
  new_connection-&gt;setBufferLimits(config_.perConnectionBufferLimitBytes());
  // 写过滤器的顺序可能需要倒置
  new_connection-&gt;setWriteFilterOrder(config_.reverseWriteFilterOrder());

  // 为连接初始化过滤器链
  const bool empty_filter_chain = !config_.filterChainFactory().createNetworkFilterChain(
      *new_connection, filter_chain-&gt;networkFilterFactories());
  // 如果初始化过滤器链失败，则关闭连接
  if (empty_filter_chain) {
    ENVOY_CONN_LOG_TO_LOGGER(parent_.logger_, debug, "closing connection: no filters", *new_connection);
    new_connection-&gt;close(Network::ConnectionCloseType::NoFlush);
    return;新连接处理的起点
  }

  // 监听器的新连接回调
  onNewConnection(std::move(new_connection));
}</pre>
<div class="blog_h3"><span class="graybg">查找过滤器链配置</span></div>
<p>在ActiveListener::newConnection期间，调用config_.filterChainManager().findFilterChain来查找匹配连接的过滤器链配置：</p>
<pre class="crayon-plain-tag">const Network::FilterChain* ListenerImpl::findFilterChain(const Network::ConnectionSocket&amp; socket) const {
  // 本地地址（恢复到原始目的地址后的）
  const auto&amp; address = socket.localAddress();

  // 根据目的端口匹配
  if (address-&gt;type() == Network::Address::Type::Ip) {
    const auto port_match = destination_ports_map_.find(address-&gt;ip()-&gt;port());
    if (port_match != destination_ports_map_.end()) {
      return findFilterChainForDestinationIP(*port_match-&gt;second.second, socket);
    }
  }

  // 缺省匹配
  const auto port_match = destination_ports_map_.find(0);
  if (port_match != destination_ports_map_.end()) {
    return findFilterChainForDestinationIP(*port_match-&gt;second.second, socket);
  }

  return nullptr;
}</pre>
<div class="blog_h3"><span class="graybg">实例化过滤器链</span></div>
<p>在ActiveListener::newConnection期间，调用config_.filterChainFactory().createNetworkFilterChain()为连接实例化过滤器链：</p>
<pre class="crayon-plain-tag">bool ListenerImpl::createNetworkFilterChain(
    Network::Connection&amp; connection,
    const std::vector&lt;Network::FilterFactoryCb&gt;&amp; filter_factories) {
  return Configuration::FilterChainUtility::buildFilterChain(connection, filter_factories);
}

// 和构建监听器过滤器时的逻辑一样，遍历过滤器工厂，传入Connection调用之
bool FilterChainUtility::buildFilterChain(Network::FilterManager&amp; filter_manager,
                                          const std::vector&lt;Network::FilterFactoryCb&gt;&amp; factories) {
  for (const Network::FilterFactoryCb&amp; factory : factories) {
    factory(filter_manager);
  }
  // 初始化所有读过滤器，也就是调用每个过滤器的onNewConnection 
  return filter_manager.initializeReadFilters();
}</pre>
<div class="blog_h3"><span class="graybg">监听器新连接回调</span></div>
<p>当ActiveListener为新连接准备好过滤器链后，会调用自身的：</p>
<pre class="crayon-plain-tag">void ConnectionHandlerImpl::ActiveListener::onNewConnection( Network::ConnectionPtr&amp;&amp; new_connection) {
  ENVOY_CONN_LOG_TO_LOGGER(parent_.logger_, debug, "new connection", *new_connection);

  // 如果新连接的状态不是已经关闭
  if (new_connection-&gt;state() != Network::Connection::State::Closed) {
    ActiveConnectionPtr active_connection(
        new ActiveConnection(*this, std::move(new_connection), parent_.dispatcher_.timeSystem()));
    // 存放到当前ActiveListener的字段中，防止析构
    active_connection-&gt;moveIntoList(std::move(active_connection), connections_);
    // 将新连接加入到连接处理器中。注意C++ 11中++是原子操作
    parent_.num_connections_++;
  }
  // 否则，新连接将在此立即析构
}</pre>
<div class="blog_h2"><span class="graybg">处理HTTP连接</span></div>
<p>对于HTTP协议， 处理TCP连接的逻辑仍然使用。</p>
<div class="blog_h3"><span class="graybg">HTTP连接管理器</span></div>
<p>在实例化过滤器链时，HTTP连接会有一个过滤器 —— HTTP连接管理器（ConnectionManagerImpl），它的工厂如下：</p>
<pre class="crayon-plain-tag">Network::FilterFactoryCb HttpConnectionManagerFilterConfigFactory::createFilterFactoryFromProtoTyped(
    const envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager&amp; roto_config,
    Server::Configuration::FactoryContext&amp; context) {
  // 线程本地的一个缓存提供者，每500ms为当前线程更新日期信息
  std::shared_ptr&lt;Http::TlsCachingDateProviderImpl&gt; date_provider =
      context.singletonManager().getTyped&lt;Http::TlsCachingDateProviderImpl&gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(date_provider), [&amp;context] {
            return std::make_shared&lt;Http::TlsCachingDateProviderImpl&gt;(context.dispatcher(),
                                                                      context.threadLocal());
          });
  // 此管理器维护RouteConfigProvider，后者提供路由信息
  std::shared_ptr&lt;Router::RouteConfigProviderManager&gt; route_config_provider_manager =
      context.singletonManager().getTyped&lt;Router::RouteConfigProviderManager&gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(route_config_provider_manager), [&amp;context] {
            return std::make_shared&lt;Router::RouteConfigProviderManagerImpl&gt;(context.admin());
          });

  // 连接管理器的配置
  std::shared_ptr&lt;HttpConnectionManagerConfig&gt; filter_config(new HttpConnectionManagerConfig(
      proto_config, context, *date_provider, *route_config_provider_manager));

  // 此Lambda捕获了上面的共享指针，因此避免了引用计数清零
  // 此Lambda即HTTP连接管理器的L4过滤器工厂
  return [route_config_provider_manager, filter_config, &amp;context,
          date_provider](Network::FilterManager&amp; filter_manager) -&gt; void {
    // 为Connection添加一个读过滤器ConnectionManagerImpl
    filter_manager.addReadFilter(Network::ReadFilterSharedPtr{new Http::ConnectionManagerImpl(
        *filter_config, context.drainDecision(), context.random(), context.httpContext(),
        context.runtime(), context.localInfo(), context.clusterManager(),
        &amp;context.overloadManager(), context.dispatcher().timeSystem())});
  };
}</pre>
<p>ConnectionManagerImpl的构造函数如下：</p>
<pre class="crayon-plain-tag">ConnectionManagerImpl::ConnectionManagerImpl(ConnectionManagerConfig&amp; config,
                                             const Network::DrainDecision&amp; drain_close,
                                             Runtime::RandomGenerator&amp; random_generator,
                                             Http::Context&amp; http_context, Runtime::Loader&amp; runtime,
                                             const LocalInfo::LocalInfo&amp; local_info,
                                             Upstream::ClusterManager&amp; cluster_manager,
                                             Server::OverloadManager* overload_manager,
                                             Event::TimeSystem&amp; time_system)
    // ConnectionManagerConfig 连接管理器的配置
    //                 ConnectionManagerStats 统计指标
    : config_(config), stats_(config_.stats()),
    // 连接持续的时长
      conn_length_(new Stats::Timespan(stats_.named_.downstream_cx_length_ms_, time_system)),
    // Network::DrainDecision，给出连接是否应该被Drain并关闭
    //                           随机数生成器                           HTTP上下文，每个服务器一个，提供Tracer等信息
      drain_close_(drain_close), random_generator_(random_generator), http_context_(http_context),
   // 能从磁盘读取Envoy运行时快照   本地环境信息        集群管理器
      runtime_(runtime), local_info_(local_info), cluster_manager_(cluster_manager),
   // ConnectionManagerListenerStats 连接管理器监听器统计信息
      listener_stats_(config_.listenerStats()),
   // 过载管理，停止接受请求、禁止Keepalive
      overload_stop_accepting_requests_ref_(
          overload_manager ? overload_manager-&gt;getThreadLocalOverloadState().getState(
                                 Server::OverloadActionNames::get().StopAcceptingRequests)
                           : Server::OverloadManager::getInactiveState()),
      overload_disable_keepalive_ref_(
          overload_manager ? overload_manager-&gt;getThreadLocalOverloadState().getState(
                                 Server::OverloadActionNames::get().DisableHttpKeepAlive)
                           : Server::OverloadManager::getInactiveState()),
    // 授时和定时器管理
      time_system_(time_system) {}</pre>
<div class="blog_h2"><span class="graybg">注册读写回调</span></div>
<p>实际负责连接的ActiveListener，会调用自己的newConnection创建新Connection对象：</p>
<pre class="crayon-plain-tag">Network::ConnectionPtr new_connection =
    parent_.dispatcher_.createServerConnection(std::move(socket), std::move(transport_socket));</pre>
<p>可以看到，创建服务器端连接的职责委托给ConnectionHandle.Dispatcher对象，连接套接字、传输套接字的所有权都被转移：</p>
<pre class="crayon-plain-tag">Network::ConnectionPtr DispatcherImpl::createServerConnection(Network::ConnectionSocketPtr&amp;&amp; socket,
                                       Network::TransportSocketPtr&amp;&amp; transport_socket) {
  ASSERT(isThreadSafe());
  return std::make_unique&lt;Network::ConnectionImpl&gt;(*this, std::move(socket),
                                                   std::move(transport_socket), true);
}</pre>
<p>可以看到连接套接字、传输套接字的所有权继续转移，给ConnectionImpl的构造函数：</p>
<pre class="crayon-plain-tag">ConnectionImpl::ConnectionImpl(Event::Dispatcher&amp; dispatcher, ConnectionSocketPtr&amp;&amp; socket,
                               TransportSocketPtr&amp;&amp; transport_socket, bool connected)
      // 传输套接字                                     连接套接字
    : transport_socket_(std::move(transport_socket)), socket_(std::move(socket)),
      // 过滤器管理器                  流信息（日志用）
      filter_manager_(*this, *this), stream_info_(dispatcher.timeSystem()),
      // 创建写缓冲
      write_buffer_(
                                                                   // 高低水位回调
          dispatcher.getWatermarkFactory().create([this]() -&gt; void { this-&gt;onLowWatermark(); },
                                                  [this]() -&gt; void { this-&gt;onHighWatermark(); })),
      dispatcher_(dispatcher), id_(next_global_id_++) {
  // 如果连接套接字的fd不可用，认为发生OOM，让进程崩掉
  RELEASE_ASSERT(ioHandle().fd() != -1, "");
  // 设置为已连接
  if (!connected) {
    connecting_ = true;
  }

  // 边缘触发，注册读写事件监听器
  file_event_ = dispatcher_.createFileEvent(
      // 传输套接字的描述符
      ioHandle().fd(), [this](uint32_t events) -&gt; void { onFileEvent(events); },
      Event::FileTriggerType::Edge, Event::FileReadyType::Read | Event::FileReadyType::Write);
  // 注册传输套接字回调
  transport_socket_-&gt;setTransportSocketCallbacks(*this);
}</pre>
<p>可以看到，当读写事件到达时，libevent会回调ConnectionImpl::onFileEvent</p>
<div class="blog_h2"><span class="graybg">触发读写回调</span></div>
<p>当发生可读、可写事件时，ConnectionImpl::onFileEvent被调用：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onFileEvent(uint32_t events) {
  // 期望连接状态为Connected，否则认为是错误，需要关闭套接字
  if (immediate_error_event_ != ConnectionEvent::Connected) {
    if (bind_error_) {
      // 绑定失败
      if (connection_stats_ &amp;&amp; connection_stats_-&gt;bind_errors_) {
        connection_stats_-&gt;bind_errors_-&gt;inc();
      }
    } else {
      // 其它错误
      ENVOY_CONN_LOG(debug, "raising immediate error", *this);
    }
    // 关闭套接字并退出
    closeSocket(immediate_error_event_);
    return;
  }

  // 关闭事件
  if (events &amp; Event::FileReadyType::Closed) {
    // 过早关闭（early close）和读操作绝不需要同时发生
    ASSERT(!(events &amp; Event::FileReadyType::Read));
    // 关闭套接字
    ENVOY_CONN_LOG(debug, "remote early close", *this);
    closeSocket(ConnectionEvent::RemoteClose);
    return;
  }

  // 可写事件
  if (events &amp; Event::FileReadyType::Write) {
    onWriteReady();
  }

  // 可读事件，由于写事件回调可能会关闭套接字（导致fd为-1），因此做个判断
  if (ioHandle().fd() != -1 &amp;&amp; (events &amp; Event::FileReadyType::Read)) {
    onReadReady();
  }
}</pre>
<div class="blog_h2"><span class="graybg">处理TCP读</span></div>
<div class="blog_h3"><span class="graybg">整体流程</span></div>
<ol>
<li>尝试循环读取，根据读取结果设置Post操作</li>
<li>处理读取到的数据</li>
<li>执行后操作</li>
</ol>
<pre class="crayon-plain-tag">void ConnectionImpl::onReadReady() {
  ENVOY_CONN_LOG(trace, "read ready", *this);

  // 断言已经连接成功
  ASSERT(!connecting_);

  // 调用传输套接字执行循环的读操作，直到没有更多数据可读，或者出错
  IoResult result = transport_socket_-&gt;doRead(read_buffer_);
  // 实际读取的字节数
  uint64_t new_buffer_size = read_buffer_.length();
  // 更新指标
  updateReadBufferStats(result.bytes_processed_, new_buffer_size);

  // 如果到达流的终点（对端关闭），但是不支持半关闭语义
  // 则后操作设置为关闭
  if ((!enable_half_close_ &amp;&amp; result.end_stream_read_)) {
    result.end_stream_read_ = false;
    result.action_ = PostIoAction::Close;
  }

  // 如果到达流终点了，或者读取的字节数不为零
  read_end_stream_ |= result.end_stream_read_;
  if (result.bytes_processed_ != 0 || result.end_stream_read_) {
    // 处理读取的数据
    onRead(new_buffer_size);
  }

  // 如果后操作应当关闭连接，或者两边都进入半关闭状态（一方关闭发送通道后，仍可接受另一方发送过来的数据）
  if (result.action_ == PostIoAction::Close || bothSidesHalfClosed()) {
    // 则关闭套接字
    closeSocket(ConnectionEvent::RemoteClose);
  }
}</pre>
<div class="blog_h3"><span class="graybg">读取到缓冲区</span></div>
<p>传输套接字的真实类型取决于传输协议（transport_protocol），默认协议是raw_buffer，对应RawBufferSocket：</p>
<pre class="crayon-plain-tag">IoResult RawBufferSocket::doRead(Buffer::Instance&amp; buffer) {
  // IO操作之后应当执行的操作，枚举Close/KeepOpen
  PostIoAction action = PostIoAction::KeepOpen;
  uint64_t bytes_read = 0;
  bool end_stream = false;
  // 循环读取
  do {
    // 尝试读取最多16K，这个16K是随便取的值
    Api::SysCallIntResult result = buffer.read(callbacks_-&gt;ioHandle().fd(), 16384);
    ENVOY_CONN_LOG(trace, "read returns: {}", callbacks_-&gt;connection(), result.rc_);
    
    // 依据系统调用返回码决定进一步操作
    if (result.rc_ == 0) {
      // 对端关闭
      end_stream = true;
      break;
    } else if (result.rc_ == -1) {
      // 远程错误（可能是没有数据可读，读完了）
      ENVOY_CONN_LOG(trace, "read error: {}", callbacks_-&gt;connection(), result.errno_);
      if (result.errno_ != EAGAIN) {
        // 错误号不是11（Try again）后操作设置为关闭
        action = PostIoAction::Close;
      }
      break;
    } else {
      // 否则，返回码是实际读取的字节数
      bytes_read += result.rc_;
      // 如果缓冲区超过限制
      if (callbacks_-&gt;shouldDrainReadBuffer()) {
        callbacks_-&gt;setReadBufferReady();
        break;
      }
    }
  } while (true);

  return {action, bytes_read, end_stream};
}</pre>
<div class="blog_h3"><span class="graybg">处理缓冲区数据</span></div>
<p>循环读取了可用的数据到缓冲区后， ConnectionImpl会调用自己的onRead方法：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onRead(uint64_t read_buffer_size) {
  // 连接不可读则返回
  if (!read_enabled_) {
    return;
  }

  // 缓冲区为空，同时没有读取到流终点
  if (read_buffer_size == 0 &amp;&amp; !read_end_stream_) {
    return;
  }

  if (read_end_stream_) {
    // 针对原始套接字的read()调用，在EOF首次（可能是对端半关闭导致）发生后，总会返回0。这里要进行判断，以免重复处理
    if (read_end_stream_raised_) {
      ASSERT(read_buffer_size == 0);
      return;
    }
    // 防止重复处理
    read_end_stream_raised_ = true;
  }
  // 转交给过滤器管理器，过滤器管理器就是Connection本身
  filter_manager_.onRead();
}</pre>
<div class="blog_h3"><span class="graybg">遍历过滤器链</span></div>
<p>如果有数据需要处理，则调用过滤器管理器的onRead方法：</p>
<pre class="crayon-plain-tag">void FilterManagerImpl::onRead() {
  // 断言上行过滤器（读取下游发来的数据）不为空
  ASSERT(!upstream_filters_.empty());
  // 传入nullptr，则从过滤器链的头部开始遍历
  onContinueReading(nullptr);
}</pre>
<p>这里的过滤器链遍历逻辑，和监听器过滤器链遍历很类似：</p>
<pre class="crayon-plain-tag">void FilterManagerImpl::onContinueReading(ActiveReadFilter* filter) {
  // 如果不设置上一个迭代的过滤器，则从头开始，否则，从下一个开始
  std::list&lt;ActiveReadFilterPtr&gt;::iterator entry;
  if (!filter) {
    entry = upstream_filters_.begin();
  } else {
    entry = std::next(filter-&gt;entry());
  }
 
  // 遍历过滤器
  for (; entry != upstream_filters_.end(); entry++) {
    // 延迟初始化，如果过滤器尚未初始化，则调用其onNewConnection
    if (!(*entry)-&gt;initialized_) {
      (*entry)-&gt;initialized_ = true;
      FilterStatus status = (*entry)-&gt;filter_-&gt;onNewConnection();
      if (status == FilterStatus::StopIteration) {
        return;
      }
    }
    // 获取先前的读缓冲区
    BufferSource::StreamBuffer read_buffer = buffer_source_.getReadBuffer();
    // 不管是可读、还是EOF，都要调用过滤器
    if (read_buffer.buffer.length() &gt; 0 || read_buffer.end_stream) {
      FilterStatus status = (*entry)-&gt;filter_-&gt;onData(read_buffer.buffer, read_buffer.end_stream);
      if (status == FilterStatus::StopIteration) {
        // 任何一个过滤器都可以终止迭代
        return;
      }
    }
  }
}</pre>
<p>过滤器链上的过滤器会被依次的调用。</p>
<div class="blog_h2"><span class="graybg">处理HTTP下游请求读</span></div>
<div class="blog_h3"><span class="graybg">HTTP连接管理器 </span></div>
<p>对于L7连接，需要调用的网络过滤器通常就是ConnectionManagerImpl：</p>
<pre class="crayon-plain-tag">Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance&amp; data, bool) {
  // 如果编解码器没有创建，则创建之
  if (!codec_) {
    // 编解码器的类型是ServerConnection
    codec_ = config_.createCodec(read_callbacks_-&gt;connection(), data, *this);
    // 更新监控指标
    if (codec_-&gt;protocol() == Protocol::Http2) {
      stats_.named_.downstream_cx_http2_total_.inc();
      stats_.named_.downstream_cx_http2_active_.inc();
    } else {
      stats_.named_.downstream_cx_http1_total_.inc();
      stats_.named_.downstream_cx_http1_active_.inc();
    }
  }

  bool redispatch;
  do {
    redispatch = false;

    try {
      // 尝试进行报文分发
      codec_-&gt;dispatch(data);
    } catch (const CodecProtocolException&amp; e) {
      // 分发出现错误
      // 执行到这里，HTTP/1.1编解码器已经发送400状态码，HTTP/2编解码器已经发送GOAWAY
      ENVOY_CONN_LOG(debug, "dispatch error: {}", read_callbacks_-&gt;connection(), e.what());
      stats_.named_.downstream_cx_protocol_error_.inc();

      // 当出现协议错误的情况下，连接上的所有流都需要重置
      resetAllStreams();
      // 刷出写缓冲区、延迟关闭
      read_callbacks_-&gt;connection().close(Network::ConnectionCloseType::FlushWriteAndDelay);
      return Network::FilterStatus::StopIteration;
    }

    // 处理入站数据可能会导致出站数据的释放，这里再次检查
    // 看此连接是否可以在未决编解码数据发送后优雅的关闭
    // 调用Network::ReadFilterCallbacks-&gt;connection().close(Network::ConnectionCloseType::FlushWriteAndDelay)
    checkForDeferredClose();

    // 对于HTTP/1编解码器来说，它会在单个消息完成后，暂停分发
    if (codec_-&gt;protocol() != Protocol::Http2) {
       // 如果没有额外流并且还有更多数据，执行重分发
      if (read_callbacks_-&gt;connection().state() == Network::Connection::State::Open &amp;&amp;
          data.length() &gt; 0 &amp;&amp; streams_.empty()) {
        redispatch = true;
      }
      // 如果仅有单个已经完成请求处理但未应答的非WebSockert流，则暂停套接字读，以apply back pressure
      if (!streams_.empty() &amp;&amp; streams_.front()-&gt;state_.remote_complete_) {
        read_callbacks_-&gt;connection().readDisable(true);
      }
    }
  } while (redispatch);

  return Network::FilterStatus::StopIteration;
}</pre>
<p>注意ConnectionManagerImpl总是会终止网络过滤器的迭代过程。</p>
<div class="blog_h3"><span class="graybg">创建编解码器</span></div>
<pre class="crayon-plain-tag">Http::ServerConnectionPtr
HttpConnectionManagerConfig::createCodec(Network::Connection&amp; connection,
                                         const Buffer::Instance&amp; data,
                                         Http::ServerConnectionCallbacks&amp; callbacks) {
  // 根据协议类型创建适当的HTTP编解码器
  switch (codec_type_) {
  case CodecType::HTTP1:
    return Http::ServerConnectionPtr{
        new Http::Http1::ServerConnectionImpl(connection, callbacks, http1_settings_)};
  case CodecType::HTTP2:
    return Http::ServerConnectionPtr{new Http::Http2::ServerConnectionImpl(
        connection, callbacks, context_.scope(), http2_settings_, maxRequestHeadersKb())};
  case CodecType::AUTO:
    return Http::ConnectionManagerUtility::autoCreateCodec(connection, data, callbacks,
                                                           context_.scope(), http1_settings_,
                                                           http2_settings_, maxRequestHeadersKb());
  }

  NOT_REACHED_GCOVR_EXCL_LINE;
}</pre>
<p>默认情况下，走的是CodecType::AUTO分支：</p>
<pre class="crayon-plain-tag">ServerConnectionPtr ConnectionManagerUtility::autoCreateCodec(
    Network::Connection&amp; connection, const Buffer::Instance&amp; data,
    ServerConnectionCallbacks&amp; callbacks, Stats::Scope&amp; scope, const Http1Settings&amp; http1_settings,
    const Http2Settings&amp; http2_settings, const uint32_t max_request_headers_kb) {
  // 基于协议探测+应用层协议协商（ALPN）来确定下一个L7协议
  // Http2::ALPN_STRING == "h2"，是HTTP/2在ALPN中的代号
  if (determineNextProtocol(connection, data) == Http2::ALPN_STRING) {
    // 使用HTTP/2协议
    return ServerConnectionPtr{new Http2::ServerConnectionImpl(
        connection, callbacks, scope, http2_settings, max_request_headers_kb)};
  } else {
    // 使用HTTP/1协议
    return ServerConnectionPtr{
        new Http1::ServerConnectionImpl(connection, callbacks, http1_settings)};
  }
}</pre>
<p>HTTP协议的版本不同，则ServerConnection的实际类型不同，对于HTTP/1来说，调用Http1::ServerConnectionImpl的构造函数：</p>
<pre class="crayon-plain-tag">ServerConnectionImpl::ServerConnectionImpl(Network::Connection&amp; connection,
                                           ServerConnectionCallbacks&amp; callbacks,
                                           Http1Settings settings)
    : ConnectionImpl(connection, HTTP_REQUEST), callbacks_(callbacks), codec_settings_(settings) {}</pre>
<p>这个函数没什么好说的，它的初始化列表中创建了ConnectionImpl，这是http::Connection的实现：</p>
<pre class="crayon-plain-tag">ConnectionImpl::ConnectionImpl(Network::Connection&amp; connection, http_parser_type type)
    // L2 Connection对象       支持水位的缓冲                    低水位回调
    : connection_(connection), output_buffer_([&amp;]() -&gt; void { this-&gt;onBelowLowWatermark(); },
    //                                                        高水位回调
                                              [&amp;]() -&gt; void { this-&gt;onAboveHighWatermark(); }) {
  // 设置水位，低水位为入参的1/2，高水位为入参
  output_buffer_.setWatermarks(connection.bufferLimit());
  // 初始化HTTP报文解析器
  http_parser_init(&amp;parser_, type);
  parser_.data = this;
}</pre>
<p>http_parser_init来自Node.js项目：</p>
<pre class="crayon-plain-tag">请求头void
http_parser_init (http_parser *parser, enum http_parser_type t)
{
  void *data = parser-&gt;data;
  memset(parser, 0, sizeof(*parser));
  parser-&gt;data = data;
  parser-&gt;type = t;
  parser-&gt;state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res));
  parser-&gt;http_errno = HPE_OK;
}</pre>
<p>此解析器比较严格，如果你的应用程序的HTTP报文头不符合规范可能导致无法解析。 </p>
<div class="blog_h3"><span class="graybg">HTTP1数据分发</span></div>
<p>HTTP连接管理器会调用ServerConnection的dispatch方法进行数据分发，后者从http1::ConnectionImpl继承的dispatch逻辑如下：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::dispatch(Buffer::Instance&amp; data) {
  ENVOY_CONN_LOG(trace, "parsing {} bytes", connection_, data.length());

  // 是否可以直接分发，仅对于Upgrade请求返回true
  if (maybeDirectDispatch(data)) {
    return;
  }

  // 总是尝试将解析器从暂停中恢复
  http_parser_pause(&amp;parser_, 0);

  ssize_t total_parsed = 0;
  if (data.length() &gt; 0) {
    // 获取原始缓冲切片，第一个参数是切片数组，第二个为数组大小，返回值是实际需要的切片数
    // 第一次调用就是为了获得实际需要的切片数
    uint64_t num_slices = data.getRawSlices(nullptr, 0);
    // #define STACK_ARRAY(var, type, num) StackArray&lt;type&gt; var(::alloca(sizeof(type) * num), num)
    // 在栈上创建数组变量slices
    STACK_ARRAY(slices, Buffer::RawSlice, num_slices);
    // 将带解析数据分到切片中
    data.getRawSlices(slices.begin(), num_slices);
    // 逐个处理切片
    for (const Buffer::RawSlice&amp; slice : slices) {
    //                              获取切片的裸数据，传递给HTTP解析器
      total_parsed += dispatchSlice(static_cast&lt;const char*&gt;(slice.mem_), slice.len_);
    }
  } else {
    dispatchSlice(nullptr, 0);
  }
  // 解析完毕，分发完毕，对应的Envoy解码也完毕
  ENVOY_CONN_LOG(trace, "parsed {} bytes", connection_, total_parsed);
  // 排干已经解析的数据
  data.drain(total_parsed);

  // 如果Upgrade请求已经被处理，并且存在：
  // 1、请求体数据
  // 2、或者early upgrade载荷
  // 需要发送，则发送之
  maybeDirectDispatch(data);
}</pre>
<p>从上面的代码我们看到，HTTP请求数据是划分为切片，逐个切片进行解析的：</p>
<pre class="crayon-plain-tag">// 切片内容示例
// GET /healthz HTTP/1.1\r\nUser-Agent: curl/7.35.0\r\nAccept: */*\r\nHost: podinfo-canary.default.svc.k8s.gmem.cc\r\n\r\n
size_t ConnectionImpl::dispatchSlice(const char* slice, size_t len) {
  ssize_t rc = http_parser_execute(&amp;parser_, &amp;settings_, slice, len);
  // 解析失败则抛出异常
  if (HTTP_PARSER_ERRNO(&amp;parser_) != HPE_OK &amp;&amp; HTTP_PARSER_ERRNO(&amp;parser_) != HPE_PAUSED) {
    sendProtocolError();
    throw CodecProtocolException("http/1.1 protocol error: " + std::string(http_errno_name(HTTP_PARSER_ERRNO(&amp;parser_))));
  }

  return rc;
}</pre>
<div class="blog_h3"><span class="graybg">HTTP1数据解析</span></div>
<p>注意：由于HTTP1不支持多路复用，因此请求解析结果信息都是以Http::Http1::ConnectionImpl的实例变量的形式存放的。</p>
<p>http_parser_execute的实现细节我们不去深究，这里主要关注一下settings_，其类型为：</p>
<pre class="crayon-plain-tag">struct http_parser_settings {
  // 在解析了HTTP报文的各个部分之后，执行对应的回调
  http_cb      on_message_begin;
  http_data_cb on_url;
  http_data_cb on_status;
  http_data_cb on_header_field;
  http_data_cb on_header_value;
  http_cb      on_headers_complete;
  http_data_cb on_body;
  http_cb      on_message_complete;
  // 调用on_chunk_header时当前chunk的长度存放在 parser-&gt;content_length
  http_cb      on_chunk_header;
  http_cb      on_chunk_complete;
};</pre>
<p>Envoy提供的settings_变量如下，注意ConnectionImpl对象调用了HTTP解析器，并且把自身传递给parser.data：</p>
<pre class="crayon-plain-tag">http_parser_settings ConnectionImpl::settings_{
    [](http_parser* parser) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onMessageBeginBase();
      return 0;
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onUrl(at, length);
      return 0;
    },
    nullptr, // on_status
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeaderField(at, length);
      return 0;
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeaderValue(at, length);
      return 0;
    },
    [](http_parser* parser) -&gt; int {
      return static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onHeadersCompleteBase();
    },
    [](http_parser* parser, const char* at, size_t length) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onBody(at, length);
      return 0;
    },
    [](http_parser* parser) -&gt; int {
      static_cast&lt;ConnectionImpl*&gt;(parser-&gt;data)-&gt;onMessageCompleteBase();
      return 0;
    },
    nullptr, // on_chunk_header
    nullptr  // on_chunk_complete
};</pre>
<p>最初被回调的是onMessageBeginBase方法，表示开始解析HTTP报文了：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onMessageBeginBase() {
  ENVOY_CONN_LOG(trace, "message begin", connection_);
  ASSERT(!current_header_map_);
  // HeaderMapImpl是为性能高度优化的Http::HeaderMap实现，尽量避免拷贝和内存分配
  current_header_map_ = std::make_unique&lt;HeaderMapImpl&gt;();
  // 解析状态，Field / Value / Done
  header_parsing_state_ = HeaderParsingState::Field;
  onMessageBegin();
}

void ServerConnectionImpl::onMessageBegin() {
  if (!resetStreamCalled()) {
    // 如果没有进行流重置，则初始化当前ActiveRequest对象
    ASSERT(!active_request_);
    active_request_ = std::make_unique&lt;ActiveRequest&gt;(*this);
    //               StreamDecoder                                        ResponseStreamEncoderImpl
    active_request_-&gt;request_decoder_ = &amp;callbacks_.newStream(active_request_-&gt;response_encoder_);
  }
}</pre>
<p>解析出URL路径后，回调：</p>
<pre class="crayon-plain-tag">void ServerConnectionImpl::onUrl(const char* data, size_t length) {
  if (active_request_) {
    active_request_-&gt;request_url_.append(data, length);
  }
}</pre>
<p>为请求设置请求URL的路径部分，例如 /healthz。</p>
<p>解析完每个请求头后，依次回调onHeaderField、onHeaderValue方法：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::onHeaderField(const char* data, size_t length) {
  if (header_parsing_state_ == HeaderParsingState::Done) {
    // 忽略 trailers
    return;
  }
  // 解析请求值后，下面的判断为true
  if (header_parsing_state_ == HeaderParsingState::Value) {
    // 完成上一个请求头的处理
    completeLastHeader();
  }

  // 暂存到一个缓冲区中
  current_header_field_.append(data, length);
}

void ConnectionImpl::onHeaderValue(const char* data, size_t length) {
  if (header_parsing_state_ == HeaderParsingState::Done) {
    // 忽略 trailers
    return;
  }
  // 设置头解析状态
  header_parsing_state_ = HeaderParsingState::Value;
  // 暂存到一个缓冲区中
  current_header_value_.append(data, length);
} </pre>
<p>在解析完最后一个请求头后会执行：</p>
<pre class="crayon-plain-tag">void ConnectionImpl::completeLastHeader() {
  if (!current_header_field_.empty()) {
    toLowerTable().toLowerCase(current_header_field_.buffer(), current_header_field_.size());
    // 存储到请求头映射中
    current_header_map_-&gt;addViaMove(std::move(current_header_field_),
                                    std::move(current_header_value_));
  }
  // 设置头解析状态
  header_parsing_state_ = HeaderParsingState::Field;
  // 由于std::move的移动语义 HeaderString 变成一个空壳子
  ASSERT(current_header_field_.empty());
  ASSERT(current_header_value_.empty());
}</pre>
<p>完成全部请求头的处理后，回调onHeadersCompleteBase：</p>
<pre class="crayon-plain-tag">int ConnectionImpl::onHeadersCompleteBase() {
  // 将最后一个请求头加入映射
  completeLastHeader();
  if (!(parser_.http_major == 1 &amp;&amp; parser_.http_minor == 1)) {
    // 如果不是HTTP/1.1，则设置协议
    protocol_ = Protocol::Http10;
  }
  if (Utility::isUpgrade(*current_header_map_)) {
    // 根据请求头判定是否客户端在请求升级协议
    handling_upgrade_ = true;
  }

  // 移动请求头映射
  int rc = onHeadersComplete(std::move(current_header_map_));
  current_header_map_.reset();
  // 设置请求头解析状态
  header_parsing_state_ = HeaderParsingState::Done;

  // 返回2，提示http_parser不去期望请求体和更多的信息
  return handling_upgrade_ ? 2 : rc;
}


int ServerConnectionImpl::onHeadersComplete(HeaderMapImplPtr&amp;&amp; headers) {
  // 需要处理响应比请求完成发生更早的情况，这种情况可能由上层代码导致
  if (active_request_) {
    const char* method_string = http_method_str(static_cast&lt;http_method&gt;(parser_.method));

    // 如果请求使用HEAD方法，则给与响应编码器以提示，便于它正确设置内容长度、传输编码等头字段
    active_request_-&gt;response_encoder_.isResponseToHeadRequest(parser_.method == HTTP_HEAD);

    // 当前CONNECT方法是不支持的，但是http_parser_parse_url需要知晓CONNECT
    handlePath(*headers, parser_.method);
    ASSERT(active_request_-&gt;request_url_.empty());
    // 添加Method头
    headers-&gt;insertMethod().value(method_string, strlen(method_string));

    // 判断请求体是否存在，这里使用新的RFC语义来判断 ——  content-length头、hunked transfer-encoding头存在
    // 意味着请求体存在 —— 而不是基于HTTP方法判断
    // 如果没有请求体，延迟对StreamDecoder.decodeHeaders()的调用，直到http解析器flush（回调onMessageComplete）
    if (parser_.flags &amp; F_CHUNKED || (parser_.content_length &gt; 0 &amp;&amp; parser_.content_length != ULLONG_MAX) || handling_upgrade_) {
      // 没有请求体，立即解码请求头
      active_request_-&gt;request_decoder_-&gt;decodeHeaders(std::move(headers), false);

      // If the connection has been closed (or is closing) after decoding headers, pause the parser
      // so we return control to the caller.
      if (connection_.state() != Network::Connection::State::Open) {
        http_parser_pause(&amp;parser_, 1);
      }

    } else {
      // 移动以便延迟解码请求头
      deferred_end_stream_headers_ = std::move(headers);
    }
  }

  return 0;
}</pre>
<p>完成整个请求处理后，回调onMessageCompleteBase： </p>
<pre class="crayon-plain-tag">void ConnectionImpl::onMessageCompleteBase() {
  if (handling_upgrade_) {
    // 如果当前是Upgrade请求则不调用onMessageComplete
    // Upgrade载荷将作为流的体看待
    ASSERT(!deferred_end_stream_headers_);
    // 暂停解析
    http_parser_pause(&amp;parser_, 1);
    return;
  }
  onMessageComplete();
}

void ServerConnectionImpl::onMessageComplete() {
  if (active_request_) {
    Buffer::OwnedImpl buffer;
    // 提示请求端消息处理完毕
    active_request_-&gt;remote_complete_ = true;

    // 如果延迟了请求头解码，这里进行解码
    if (deferred_end_stream_headers_) {
      active_request_-&gt;request_decoder_-&gt;decodeHeaders(std::move(deferred_end_stream_headers_),
                                                       true);
      deferred_end_stream_headers_.reset();
    } else {
    // 否则，解码数据
      active_request_-&gt;request_decoder_-&gt;decodeData(buffer, true);
    }
  }

  // 总是暂停HTTP解析器，这样调用者同时只能处理单个请求，从而施加反向压力（apply back pressure）
  // 调用者需要检测缓冲中有更多的数据，并进行再次分发
  http_parser_pause(&amp;parser_, 1);
}</pre>
<div class="blog_h3"><span class="graybg">HTTP请求头解码 </span></div>
<p>经过上一节的分析，我们了解到，当HTTP解析器处理完请求后，会调用ServerConnectionImpl::onMessageComplete回调，该回调则会调用ActiveStream（实现了StreamDecoder）进行请求解码。</p>
<p>这个请求解码是Envoy上下文的，它会执行Envoy的核心代理逻辑 —— 遍历HTTP过滤器链、进行路由选择：</p>
<pre class="crayon-plain-tag">// 该函数的逻辑顺序很复杂，但也很重要
//
// 我们希望在选路之前做尽量少的工作，并且创建一个过滤器链来最大化需要定制过滤器行为—— 例如注册访问日志器 ——的请求数量
// 要达成目标，需要在以下几个事项之间进行权衡：
// 1、对无效请求进行合法性检查，因为无效请求可能因为没有完整的头信息而无法选路
// 2、检查服务器错误响应（连接关闭、HEAD请求...）所需要的状态
// 3、过滤器对请求本身的、可能影响选路的修改
//
void ConnectionManagerImpl::ActiveStream::decodeHeaders(HeaderMapPtr&amp;&amp; headers, bool end_stream) {
  // 移动请求头为ActiveStream的字段
  request_headers_ = std::move(headers);
  if (Http::Headers::get().MethodValues.Head == request_headers_-&gt;Method()-&gt;value().c_str()) {
    // 判断是否HEAD请求
    is_head_request_ = true;
  }
  ENVOY_STREAM_LOG(debug, "request headers complete (end_stream={}):\n{}", *this, end_stream, *request_headers_);

  // 如果请求仅有请求头（header-only，没有体），则在此可以结束解码流程
  // 如果我们将请求转换为header-only，则一旦后续的decodeData/decodeTrailers被调用则当前流就被标记为完成
  // 下面的方法设置remote_complete_ = end_stream
  maybeEndDecode(end_stream);

  // 如果过载了，只要解码了请求头，就丢弃请求
  // 连接管理器是为当前L4连接服务的，它是一个网络过滤器。当出现过载后，其overload_stop_accepting_requests_ref_ == Active
  if (connection_manager_.overload_stop_accepting_requests_ref_ == Server::OverloadActionState::Active) {
    // 在此特殊分支下，不去创建过滤器链 —— 如果存在内存过载风险更重要的是避免内存分配，而非创建过滤器
    // 标记为过滤器已创建
    state_.created_filter_chain_ = true;
    connection_manager_.stats_.named_.downstream_rq_overload_close_.inc();
    // 由Envoy直接应答下游
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_),
                   Http::Code::ServiceUnavailable, "envoy overloaded", nullptr, is_head_request_,
                   absl::nullopt);
    return;
  }
  //                               是否Envoy需要代理Expect: 100-Continue
  if (!connection_manager_.config_.proxy100Continue() &amp;&amp; request_headers_-&gt;Expect() &amp;&amp;
      request_headers_-&gt;Expect()-&gt;value() == Headers::get().ExpectValues._100Continue.c_str()) {
    // 执行到这里意味着Envoy在处理100-Continue，跳过过滤器链，直接发送100-Continue给编码器
    // 100-Continue用于客户端在发送POST数据给服务器前，征询服务器情况，看服务器是否处理POST的数据，
    // 如果不处理，客户端则不上传POST数据，如果处理，则POST上传数据。在现实应用中，通常在POST大数据时，
    // 才会使用100-continue协议
    // 服务器端的行为应该是：返回100-Continue表示自己能够处理POST数据，或者错误码

    // 执行一些统计指标收集
    chargeStats(continueHeader());

    // 执行响应编码
    response_encoder_-&gt;encode100ContinueHeaders(continueHeader());
    // 移除Expect头，防止在上游再次处理
    request_headers_-&gt;removeExpect();
  }

  // 从请求头中读取UserAgent —— 针对特定user agent的统计指标
  connection_manager_.user_agent_.initializeFromHeaders(
      *request_headers_, connection_manager_.stats_.prefix_, connection_manager_.stats_.scope_);

  // 确保codec版本（HTTP协议版本）是支持的
  Protocol protocol = connection_manager_.codec_-&gt;protocol();
  if (protocol == Protocol::Http10) {
    // 这种情况下，HTTP/1.x中除了1.1都可以
    stream_info_.protocol(protocol);
    if (!connection_manager_.config_.http1Settings().accept_http_10_) {
      // 如果配置中没有显式支持HTTP/1.0，发送Envoy本地响应Upgrade Required
      sendLocalReply(false, Code::UpgradeRequired, "", nullptr, is_head_request_, absl::nullopt);
      return;
    } else {
      // HTTP/1.0 默认不支持连接复用，除非请求头指定Keep-Alive，需要保证连接关闭
      state_.saw_connection_close_ = true;
      if (request_headers_-&gt;Connection() &amp;&amp;
          absl::EqualsIgnoreCase(request_headers_-&gt;Connection()-&gt;value().getStringView(),
                                 Http::Headers::get().ConnectionValues.KeepAlive)) {
        state_.saw_connection_close_ = false;
      }
    }
  }
  // 如果缺少Host头
  if (!request_headers_-&gt;Host()) {
    if ((protocol == Protocol::Http10) &amp;&amp; !connection_manager_.config_.http1Settings().default_host_for_http_10_.empty()) {
      // 当前是HTTP10且配置了缺省Host头，则设置此头
      request_headers_-&gt;insertHost().value(connection_manager_.config_.http1Settings().default_host_for_http_10_);
    } else {
      // 非HTTP10，必须有Host头，对于HTTP11来说Host头重命名为:authority
      // Envoy本地应答
      sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::BadRequest, "", nullptr, is_head_request_, absl::nullopt);
      return;
    }
  }
  
  // 处理请求头部过长的情况
  ASSERT(connection_manager_.config_.maxRequestHeadersKb() &gt; 0);
  if (request_headers_-&gt;byteSize() &gt; (connection_manager_.config_.maxRequestHeadersKb() * 1024)) {
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_),
                   Code::RequestHeaderFieldsTooLarge, "", nullptr, is_head_request_, absl::nullopt);
    return;
  }

  // 当前在应用层，我们仅仅支持相对路径。在这里预期codec已经把路径打散成片
  // 注意：目前HTTP11 codec仅在allow_absolute_url标记启用的情况下才进行打散操作
  //  我们也会检查:path头，因为CONNECT请求没有URL路径，而当前不支持CONNECT请求
  if (!request_headers_-&gt;Path() || request_headers_-&gt;Path()-&gt;value().c_str()[0] != '/') {
    connection_manager_.stats_.named_.downstream_rq_non_relative_path_.inc();
    sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::NotFound, "", nullptr,
                   is_head_request_, absl::nullopt);
    return;
  }

  // 对于HTTP11，如果请求头Connection: Close，表示不启用keep-Alive
  if (protocol == Protocol::Http11 &amp;&amp; request_headers_-&gt;Connection() &amp;&amp;
      absl::EqualsIgnoreCase(request_headers_-&gt;Connection()-&gt;value().getStringView(),
                             Http::Headers::get().ConnectionValues.Close)) {
    // 那么意味着客户端已经关闭连接
    state_.saw_connection_close_ = true;
  }

  // 如果当前请求不是内部重定向
  if (!state_.is_internally_created_) { // Only sanitize headers on first pass.
    // 根据配置、请求头来修改下游连接的远程地址（客户端地址）
    // 日志目的
    stream_info_.setDownstreamRemoteAddress(ConnectionManagerUtility::mutateRequestHeaders(
        *request_headers_, connection_manager_.read_callbacks_-&gt;connection(),
        connection_manager_.config_, *snapped_route_config_, connection_manager_.random_generator_,
        connection_manager_.runtime_, connection_manager_.local_info_));
  }
  ASSERT(stream_info_.downstreamRemoteAddress() != nullptr);

  ASSERT(!cached_route_);
  // 刷新缓存的路由（条目），可能设置cached_cluster_info_ —— 目标上游集群信息，意味着选路可能完成
  refreshCachedRoute();
  const bool upgrade_rejected = createFilterChain() == false;

  // TODO 如果在准备过滤器迭代时，发现链中没有任何过滤器，连接管理器应该返回404，当前实现时不返回响应
  if (protocol == Protocol::Http11 &amp;&amp; cached_route_.value()) {
    if (upgrade_rejected) {
      // 当前路由不支持升级，因此发送Envoy本地响应
      connection_manager_.stats_.named_.downstream_rq_ws_on_non_ws_route_.inc();
      sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::Forbidden, "",
                     nullptr, is_head_request_, absl::nullopt);
      return;
    }
    // 允许WebSocket请求穿过启用了WebSocket支持的路由
  }

  // 如果有路由，且路由配置了idle超时
  if (cached_route_.value()) {
    const Router::RouteEntry* route_entry = cached_route_.value()-&gt;routeEntry();
    if (route_entry != nullptr &amp;&amp; route_entry-&gt;idleTimeout()) {
      idle_timeout_ms_ = route_entry-&gt;idleTimeout().value();
      if (idle_timeout_ms_.count()) {
        // 如果流超时定时器没创建，则创建之
        if (stream_idle_timer_ == nullptr) {
          stream_idle_timer_ =
              connection_manager_.read_callbacks_-&gt;connection().dispatcher().createTimer(
                  [this]() -&gt; void { onIdleTimeout(); });
        }
      } else if (stream_idle_timer_ != nullptr) {
        // 如果存在流超时定时器，但是路由的idle超时为0，则禁用定时器
        stream_idle_timer_ = nullptr;
      }
    }
  }

  // 进行请求追踪
  if (connection_manager_.config_.tracingConfig()) {
    traceRequest();
  }

  // 解码请求头
  decodeHeaders(nullptr, *request_headers_, end_stream);

  // 重置超时定时器
  resetIdleTimer();
}</pre>
<p>请求头的解码逻辑位于decodeHeaders方法中，上面的方法传入的第一个参数是nullptr，表示从头开始迭代过滤器链：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::decodeHeaders(ActiveStreamDecoderFilter* filter,
                                                        HeaderMap&amp; headers, bool end_stream) {
  // 从头，或者从指定过滤器开始迭代
  std::list&lt;ActiveStreamDecoderFilterPtr&gt;::iterator entry;
  std::list&lt;ActiveStreamDecoderFilterPtr&gt;::iterator continue_data_entry = decoder_filters_.end();
  if (!filter) {
    entry = decoder_filters_.begin();
  } else {
    entry = std::next(filter-&gt;entry());
  }

  // 遍历剩下的过滤器
  for (; entry != decoder_filters_.end(); entry++) {
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::DecodeHeaders));
    // 设置状态位
    state_.filter_call_state_ |= FilterCallState::DecodeHeaders;
    (*entry)-&gt;end_stream_ =
    // 仅仅解码请求头，或者传入end_stream=true（表示这是header-only的请求）
        decoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == decoder_filters_.end());
    // 调用过滤器来解码请求头，返回的状态决定后续流程走向
    FilterHeadersStatus status = (*entry)-&gt;decodeHeaders(headers, (*entry)-&gt;end_stream_);
    // ContinueAndEndStream表示继续迭代后续过滤器，但是忽略后续的请求体、尾 —— 这意味着创建header-only请求/应答
    ASSERT(!(status == FilterHeadersStatus::ContinueAndEndStream &amp;&amp; (*entry)-&gt;end_stream_));
    // 清空状态位
    state_.filter_call_state_ &amp;= ~FilterCallState::DecodeHeaders;
    ENVOY_STREAM_LOG(trace, "decode headers called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 处理请求头的回调被调用后的通用处理逻辑：
    // 根据status设置ActiveStream的一些字段，例如stopped_、headers_only、headers_continued_
    // 只有返回true，才可能继续迭代
    if (!(*entry)-&gt;commonHandleAfterHeadersCallback(status, decoding_headers_only_) &amp;&amp;
        std::next(entry) != decoder_filters_.end()) {
      // 如果当前不是最后一个过滤器，停止迭代。否则，还需要继续处理先前过滤器添加了体的情况
      return;
    }

    // 这里处理特殊的情况：我们使用header-only请求，但是前面的过滤器填充了请求体
    // 这意味着不能在内联迭代（inline iteration）阶段再向后面的过滤器传递end_stream = true了
    if (end_stream &amp;&amp; buffered_request_data_ &amp;&amp; continue_data_entry == decoder_filters_.end()) {
      // 设置下一个执行的过滤器（为当前过滤器）
      continue_data_entry = entry;
    }
  }

  if (continue_data_entry != decoder_filters_.end()) {
    // 从当前过滤器继续迭代，调用continueDecoding()以防再调用decodeHeaders()
    ASSERT(buffered_request_data_);
    // 仿冒stopped_ = true，因为continueDecoding() 期望之
    (*continue_data_entry)-&gt;stopped_ = true;
    // 使用缓冲的请求头、体数据继续迭代
    (*continue_data_entry)-&gt;continueDecoding();
  }

  if (end_stream) {
    // 解除超时过滤器
    disarmRequestTimeout();
  }
}</pre>
<p>单个过滤器解码请求头的逻辑由ActiveStreamDecoderFilter.decodeHeaders提供：</p>
<pre class="crayon-plain-tag">FilterHeadersStatus decodeHeaders(HeaderMap&amp; headers, bool end_stream) {
  is_grpc_request_ = Grpc::Common::hasGrpcContentType(headers);
  return handle_-&gt;decodeHeaders(headers, end_stream);
}</pre>
<p>可以看到，它只是判断一下是否gRPC请求，然后就转交给 StreamDecoderFilter handle_，这个handle是一个个具体的HTTP过滤器。</p>
<p>HTTP过滤器可能对请求头进行任意的操作，例如修改某个头，最终它会返回下面的枚举值之一：</p>
<pre class="crayon-plain-tag">enum class FilterHeadersStatus {
  // 继续迭代下一个过滤器
  Continue,
  // 不再迭代后续过滤器
  StopIteration,
  // 继续迭代下一个过滤器，但是不忽略报文体、尾，也就是创建header-only的请求/响应
  ContinueAndEndStream
};</pre>
<p>返回值会影响如何进行后续的过滤器迭代。</p>
<div class="blog_h3"><span class="graybg">创建过滤器链</span></div>
<p>此方法考虑了协议升级的情况：</p>
<pre class="crayon-plain-tag">bool ConnectionManagerImpl::ActiveStream::createFilterChain() {
  // 过滤器链已经创建则返回，HTTP过滤器链只有一个（相对于单个HTTP连接管理器），而不像网络过滤器，可以有多个
  if (state_.created_filter_chain_) {
    return false;
  }
  bool upgrade_rejected = false;
  // 升级的目标协议
  auto upgrade = request_headers_ ? request_headers_-&gt;Upgrade() : nullptr;
  // 标记为过滤器已创建
  state_.created_filter_chain_ = true;
  if (upgrade != nullptr) {
    // 需要进行协议升级判断
    const Router::RouteEntry::UpgradeMap* upgrade_map = nullptr;
    // 设置UpgradeMap，包含路由条目支持的升级协议信息
    if (cached_route_.has_value() &amp;&amp; cached_route_.value() &amp;&amp; cached_route_.value()-&gt;routeEntry()) {
      upgrade_map = &amp;cached_route_.value()-&gt;routeEntry()-&gt;upgradeMap();
    }
    // 创建升级的过滤器链
    if (connection_manager_.config_.filterFactory().createUpgradeFilterChain( upgrade-&gt;value().c_str(), upgrade_map, *this)) {
      state_.successful_upgrade_ = true;
      connection_manager_.stats_.named_.downstream_cx_upgrades_total_.inc();
      connection_manager_.stats_.named_.downstream_cx_upgrades_active_.inc();
      return true;
    } else {
      upgrade_rejected = true;
      // 失败，退化为默认过滤器链，调用者将会发送Envoy本地响应提示升级失败
    }
  }
  // 创建默认过滤器链
  connection_manager_.config_.filterFactory().createFilterChain(*this);
  return !upgrade_rejected;
}</pre>
<p>默认过滤器链在下面的方法中创建：</p>
<pre class="crayon-plain-tag">void HttpConnectionManagerConfig::createFilterChain(Http::FilterChainFactoryCallbacks&amp; callbacks) {
  for (const Http::FilterFactoryCb&amp; factory : filter_factories_) {
    factory(callbacks);
  }
}</pre>
<p>可以看到，和网络过滤器一样的模式，调用各过滤器提供的工厂，传输FilterChainFactoryCallbacks。 </p>
<div class="blog_h2"><span class="graybg">和HTTP上游集群交互 </span></div>
<div class="blog_h3"><span class="graybg">HTTP路由处理</span></div>
<p>最后一个HTTP过滤器通常都是Envoy::Router::Filter，此过滤器决定如何转发下游请求给上游集群。毕竟Envoy只是个代理，它不负责实质性的请求处理。 </p>
<pre class="crayon-plain-tag">Http::FilterHeadersStatus Filter::decodeHeaders(Http::HeaderMap&amp; headers, bool end_stream) {
  // 确保向上游发送的出站请求需要的HTTP/2头都存在
  ASSERT(headers.Path());
  ASSERT(headers.Method());
  ASSERT(headers.Host());

  // 来自下游的头
  downstream_headers_ = &amp;headers;

  // 是否为gRPC请求
  grpc_request_ = Grpc::Common::hasGrpcContentType(headers);

  // 增加rq_total计数
  config_.stats_.rq_total_.inc();

  // 获取路由
  route_ = callbacks_-&gt;route();
  if (!route_) {
    // 增加no_route计数
    config_.stats_.no_route_.inc();
    ENVOY_STREAM_LOG(debug, "no cluster match for URL '{}'", *callbacks_, headers.Path()-&gt;value().c_str());
    // 记录没有路由这一情况到StreamInfo
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoRouteFound);
    // 设置本地响应
    callbacks_-&gt;sendLocalReply(Http::Code::NotFound, "", nullptr, absl::nullopt);
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 如果有请求的直接响应，则返回直接响应，否则返回nullptr
  // 直接响应即Envoy自己生成的响应，而非代理上游集群的
  const auto* direct_response = route_-&gt;directResponseEntry();
  if (direct_response != nullptr) {
    config_.stats_.rq_direct_response_.inc();
    // 重写Path头
    direct_response-&gt;rewritePathHeader(headers, !config_.suppress_envoy_headers_);
    // 发送本地响应
    callbacks_-&gt;sendLocalReply(
        // 使用直接响应的头、体
        direct_response-&gt;responseCode(), direct_response-&gt;responseBody(),
        // 修改响应头的Lambda
        [this, direct_response,
         &amp;request_headers = headers](Http::HeaderMap&amp; response_headers) -&gt; void {
          // 基于请求头得到重定向路径
          const auto new_path = direct_response-&gt;newPath(request_headers);
          if (!new_path.empty()) {
            // 添加头
            response_headers.addReferenceKey(Http::Headers::get().Location, new_path);
          }
          // 在转发之前，进行可能是破坏性的响应头转换，例如添加/删除头
          // 只能在获取所有初始响应头后调用单次
          direct_response-&gt;finalizeResponseHeaders(response_headers, callbacks_-&gt;streamInfo());
        },
        absl::nullopt);
    // Router过滤器总是停止迭代
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 匹配请求的路由条目
  route_entry_ = route_-&gt;routeEntry();
  // 从集群管理器cm_中后去路由条目提供的山有集群名称，例如 outbound|9898||podinfo-canary.default.svc.k8s.gmem.cc
  Upstream::ThreadLocalCluster* cluster = config_.cm_.get(route_entry_-&gt;clusterName());
  if (!cluster) {
    // 找不到集群
    config_.stats_.no_cluster_.inc();
    ENVOY_STREAM_LOG(debug, "unknown cluster '{}'", *callbacks_, route_entry_-&gt;clusterName());
    // 记录错误并进行本地应答
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoRouteFound);
    callbacks_-&gt;sendLocalReply(route_entry_-&gt;clusterNotFoundResponseCode(), "", nullptr, absl::nullopt);
    return Http::FilterHeadersStatus::StopIteration;
  }
  // 从线程本地的cluster获得ClusterInfo对象，此对象可以安全的超越ThreadLocalCluster的生命周期存在
  cluster_ = cluster-&gt;info();

  // 虚拟上游集群，根据请求路径确定
  request_vcluster_ = route_entry_-&gt;virtualCluster(headers);
  ENVOY_STREAM_LOG(debug, "cluster '{}' match for URL '{}'", *callbacks_, route_entry_-&gt;clusterName(), headers.Path()-&gt;value().c_str());

  // 上游集群的统计指标的备选前缀
  const Http::HeaderEntry* request_alt_name = headers.EnvoyUpstreamAltStatName();
  if (request_alt_name) {
    alt_stat_prefix_ = std::string(request_alt_name-&gt;value().c_str()) + ".";
    headers.removeEnvoyUpstreamAltStatName();
  }

  // 看看是不是应该立即杀死一定比例的、此集群的流量
  // maintenanceMode()返回集群是否出于维护模式，出于此模式则不应该作为路由的目标，过滤器
  // 可以根据自己的需要来处理此调用的返回值。此方法的实现可能引入某种随机性，不会每次返回一致的值
  if (cluster_-&gt;maintenanceMode()) {
    // 上游服务器资源溢出，流需要被重置
    callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    chargeUpstreamCode(Http::Code::ServiceUnavailable, nullptr, true);
    // 进行本地应答
    callbacks_-&gt;sendLocalReply(Http::Code::ServiceUnavailable, "maintenance mode",
                               [this](Http::HeaderMap&amp; headers) {
                                 if (!config_.suppress_envoy_headers_) {
                                   // 添加Envoy特殊响应头
                                   headers.insertEnvoyOverloaded().value( Http::Headers::get().EnvoyOverloadedValues.True);
                                 }
                               },
                               absl::nullopt);
    cluster_-&gt;stats().upstream_rq_maintenance_mode_.inc();
    return Http::FilterHeadersStatus::StopIteration;
  }

  // 获取上游集群的连接池
  Http::ConnectionPool::Instance* conn_pool = getConnPool();
  if (!conn_pool) {
    // 如果无法得到/创建线程池，所有该集群没有任何可用（健康的）端点
    // 发送本地响应
    sendNoHealthyUpstreamResponse();
    return Http::FilterHeadersStatus::StopIteration;
  }

  /* 开始向上游集群的主机发送请求 */

  // 根据路由配置和请求头来决定实际使用的请求超时时间。请求头中的超时优先级更高
  timeout_ = FilterUtility::finalTimeout(*route_entry_, headers, !config_.suppress_envoy_headers_, grpc_request_);

  // 如果请求头x-envoy-upstream-rq-timeout-alt-response存在，则在请求上游超时后
  if (headers.EnvoyUpstreamRequestTimeoutAltResponse()) {
    // 设置响应码
    timeout_response_code_ = Http::Code::NoContent;
    // 同时移除x-envoy-upstream-rq-timeout-alt-response头
    headers.removeEnvoyUpstreamRequestTimeoutAltResponse();
  }

  // 如果此RouteEntry所属的虚拟主机的配置要求在上游请求中添加x-envoy-attempt-count头，则添加之
  include_attempt_count_ = route_entry_-&gt;includeAttemptCount();
  if (include_attempt_count_) {
    headers.insertEnvoyAttemptCount().value(attempt_count_);
  }

  // 将当前Span的追踪上下文注入到请求头
  callbacks_-&gt;activeSpan().injectContext(headers);
  // 在转发请求前，进行可能是销毁性的请求头转换 —— 例如URL重写、添加额外的头、删除头
  // 此方法必须仅在转发前调用单次
  route_entry_-&gt;finalizeRequestHeaders(headers, callbacks_-&gt;streamInfo(), !config_.suppress_envoy_headers_);

  // 设置Scheme头，HTTP和HTTPS
  FilterUtility::setUpstreamScheme(headers, *cluster_);

  // 重试状态
  retry_state_ = createRetryState(route_entry_-&gt;retryPolicy(), headers, *cluster_, config_.runtime_,
                       config_.random_, callbacks_-&gt;dispatcher(), route_entry_-&gt;priority());
  // 请求是否应该被shadow（镜像）
  do_shadowing_ = FilterUtility::shouldShadow(route_entry_-&gt;shadowPolicy(), config_.runtime_,  callbacks_-&gt;streamId());

  ENVOY_STREAM_LOG(debug, "router decoding headers:\n{}", *callbacks_, headers);
  // 上游请求对象
  upstream_request_ = std::make_unique&lt;UpstreamRequest&gt;(*this, *conn_pool);
  // 上游请求不会在本地走过滤器链，下面的方法仅仅是
  // 1、调用conn_pool_.newStream()创建新的流
  // 2、将新的流赋值给UpstreamRequest.conn_pool_stream_handle_变量
  upstream_request_-&gt;encodeHeaders(end_stream);
  if (end_stream) {
    // 执行此回调，用于上游请求以异步发送的，这里不代表上游请求处理完毕
    // 在Dispatcher上注册超时定时器，在上游请求执行超时后回调onResponseTimeout
    onRequestComplete();
  }

  return Http::FilterHeadersStatus::StopIteration;
}</pre>
<p>如果选择的路由的上游集群没有健康的端点，则会调用：</p>
<pre class="crayon-plain-tag">void Filter::sendNoHealthyUpstreamResponse() {
  callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoHealthyUpstream);
  chargeUpstreamCode(Http::Code::ServiceUnavailable, nullptr, false);
  callbacks_-&gt;sendLocalReply(Http::Code::ServiceUnavailable, "no healthy upstream", nullptr, absl::nullopt);
}</pre>
<p>给与客户端503响应，响应体设置为 no healthy upstream 。</p>
<div class="blog_h3"><span class="graybg">上游集群连接池</span></div>
<p>Envoy和上游集群主机的交互，是通过连接池进行的。每个上游主机对应一个连接池对象，根据协议和配置的不同，连接池中维持的连接数量也不同。对于HTTP/2协议，由于多路复用的关系，不考虑套接字选项的情况下，池中总是只有单个连接。</p>
<p>路由过滤器会调用getConnPool()来获取连接池：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance* Filter::getConnPool() {
  // 获取集群支持的特性，位域字段
  auto features = cluster_-&gt;features();
  // 根据上游集群的配置、下游连接的类型来决定使用什么协议
  // 根据运行时配置，集群可能将HTTP2降级为HTTP1
  Http::Protocol protocol;
  if (features &amp; Upstream::ClusterInfo::Features::USE_DOWNSTREAM_PROTOCOL) {
    // 如果使用下游的协议
    protocol = callbacks_-&gt;streamInfo().protocol().value();
  } else {
    // 否则，如果上游支持HTTP2则使用之，不支持则HTTP11
    protocol = (features &amp; Upstream::ClusterInfo::Features::HTTP2) ? Http::Protocol::Http2
                                                                   : Http::Protocol::Http11;
  }
  // cm_是集群管理器
  return config_.cm_.httpConnPoolForCluster(route_entry_-&gt;clusterName(), route_entry_-&gt;priority(),
                                            protocol, this);
}</pre>
<p>连接池的管理，实际上由集群管理器负责：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance*
ClusterManagerImpl::httpConnPoolForCluster(const std::string&amp; cluster, ResourcePriority priority,
                                           Http::Protocol protocol, LoadBalancerContext* context) {
  // 获取线程本地的集群管理器对象
  ThreadLocalClusterManagerImpl&amp; cluster_manager = tls_-&gt;getTyped&lt;ThreadLocalClusterManagerImpl&gt;();
  // 根据名称查找上游集群
  auto entry = cluster_manager.thread_local_clusters_.find(cluster);
  if (entry == cluster_manager.thread_local_clusters_.end()) {
    return nullptr;
  }

  // 委托给上游集群
  return entry-&gt;second-&gt;connPool(priority, protocol, context);
} </pre>
<p>获取连接池的工作进一步委托给上游集群（ClusterEntry）：</p>
<pre class="crayon-plain-tag">Http::ConnectionPool::Instance*
ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::connPool(
    ResourcePriority priority, Http::Protocol protocol, LoadBalancerContext* context) {
  // 根据负载均衡上下文（就是Router这个过滤器），也就是负载均衡策略，来决定使用哪个主机的连接池
  HostConstSharedPtr host = lb_-&gt;chooseHost(context);
  if (!host) {
    ENVOY_LOG(debug, "no healthy host for HTTP connection pool");
    cluster_info_-&gt;stats().upstream_cx_none_healthy_.inc();
    return nullptr;
  }

  // 从下游连接继承套接字选项
  std::vector&lt;uint8_t&gt; hash_key = {uint8_t(protocol), uint8_t(priority)};

  // 基于下游套接字选项来计算连接池的哈希键。以便基于套接字选项来控制连接池，让不同选项的连接不池化在一起
  bool have_options = false;
  if (context &amp;&amp; context-&gt;downstreamConnection()) {
    const Network::ConnectionSocket::OptionsSharedPtr&amp; options =
        context-&gt;downstreamConnection()-&gt;socketOptions();
    if (options) {超时
      for (const auto&amp; option : *options) {
        have_options = true;
        option-&gt;hashKey(hash_key);
      }
    }
  }

  // 获取单个主机的连接池容器
  ConnPoolsContainer&amp; container = *parent_.getHttpConnPoolsContainer(host, true);

  // 根据套接字选项的哈希，从连接池容器中获得连接池
  Http::ConnectionPool::Instance&amp; pool = container.pools_-&gt;getPool(hash_key, [&amp;]() {
    return parent_.parent_.factory_.allocateConnPool(
        parent_.thread_local_dispatcher_, host, priority, protocol,
        have_options ? context-&gt;downstreamConnection()-&gt;socketOptions() : nullptr);
  });

  return &amp;pool;
}</pre>
<div class="blog_h3"><span class="graybg">准备上游请求</span></div>
<p>获得可用的连接池对象后，Router过滤器会创建UpstreamRequest ，并调用它的encodeHeaders方法：</p>
<pre class="crayon-plain-tag">// encodeHeaders不需要变量过滤器链，因为过滤器链是为下游连接服务的
void Filter::UpstreamRequest::encodeHeaders(bool end_stream) {
  ASSERT(!encode_complete_);
  encode_complete_ = end_stream;

  // 创建一个新的流，并赋值给UpstreamRequest.conn_pool_stream_handle_
  // 注意UpstreamRequest实现了StreamDecoder，能够解码上游响应
  Http::ConnectionPool::Cancellable* handle = conn_pool_.newStream(*this, *this);
  if (handle) {
    // 可能在newStream()调用期间发生reset，这种情况下handle为nullptr
    conn_pool_stream_handle_ = handle;
  }
}</pre>
<p>连接池的newStream方法创建一个连接到某个上游主机的新的流：</p>
<pre class="crayon-plain-tag">/**
   * 在连接池上创建一个新的流
   * @param response_decoder 响应解码器 —— 对于上游请求，Router过滤器需要对其返回的应答进行解码
   * @param cb 当连接准备好和失败时执行的回调，如果有可用的连接/出现立即的失败，这些回调可能在当前方法的上下文中直接调用
   *           这种情况下，此函数返回nullptr
   * @return Cancellable* 如果池中没有可用的连接，上述cb不会被立即调用，该方法会返回一个Cancellable类型的handle
   *                      调用者可以使用该句柄来取消请求
   *                      注意：一旦任何回调函数被调用，则句柄不再有效。要取消请求，必须将流重置
   */
  virtual Cancellable* newStream(Http::StreamDecoder&amp; response_decoder, Callbacks&amp; callbacks) PURE;</pre>
<p>上述方法的实现如下：</p>
<pre class="crayon-plain-tag">ConnectionPool::Cancellable* ConnPoolImpl::newStream(StreamDecoder&amp; response_decoder,
                                                     ConnectionPool::Callbacks&amp; callbacks) {
  // 统计指标收集
  host_-&gt;cluster().stats().upstream_rq_total_.inc();
  host_-&gt;stats().rq_total_.inc();
  if (!ready_clients_.empty()) {
    // 如果有可用的客户端，则取出一个放到不可用列表中
    ready_clients_.front()-&gt;moveBetweenLists(ready_clients_, busy_clients_);
    ENVOY_CONN_LOG(debug, "using existing connection", *busy_clients_.front()-&gt;codec_client_);
    // 然后将请求关联到客户端
    attachRequestToClient(*busy_clients_.front(), response_decoder, callbacks);
    return nullptr;
  }

  //                   ResourceManager非完全一致的同步最大连接数、未决请求等信息
  //                                              是否可以创建新的请求
  if (host_-&gt;cluster().resourceManager(priority_).pendingRequests().canCreate()) {
    //                                            是否可以创建新的连接
    bool can_create_connection = host_-&gt;cluster().resourceManager(priority_).connections().canCreate();
    if (!can_create_connection) {
      // 连接总数超标
      host_-&gt;cluster().stats().upstream_cx_overflow_.inc();
    }

    // 如果池中根本没有连接，则立即创建一个防止饥饿
    if ((ready_clients_.size() == 0 &amp;&amp; busy_clients_.size() == 0) || can_create_connection) {
      // 创建新的客户端ActiveClient
      // 将其放入busy_clients_列表
      createNewConnection();
    }
    // 创建请求并排队
    return newPendingRequest(response_decoder, callbacks);
  } else {
    // 超过允许的未决请求的最大数量
    ENVOY_LOG(debug, "max pending requests overflow");
    callbacks.onPoolFailure(ConnectionPool::PoolFailureReason::Overflow, nullptr);
    host_-&gt;cluster().stats().upstream_rq_pending_overflow_.inc();
    return nullptr;
  }
} </pre>
<p>可以看到，如果连接池有空闲的HTTP客户端，则将UpstreamRequest关联到一个空闲连接：</p>
<pre class="crayon-plain-tag">void ConnPoolImpl::attachRequestToClient(ActiveClient&amp; client, StreamDecoder&amp; response_decoder,
                                         ConnectionPool::Callbacks&amp; callbacks) {
  ASSERT(!client.stream_wrapper_);
  // 将UpstreamRequest+ActiveClient封装为流编解码包装器
  client.stream_wrapper_ = std::make_unique&lt;StreamWrapper&gt;(response_decoder, client);
  // 回调onPoolReady：当连接池中有连接能够处理上游请求时执行
  callbacks.onPoolReady(*client.stream_wrapper_, client.real_host_description_);
}

// StreamWrapper的构造函数：
ConnPoolImpl::StreamWrapper::StreamWrapper(StreamDecoder&amp; response_decoder, ActiveClient&amp; parent)
    // CodecClient支持多种HTTP协议类型下的多路流、底层连接的管理
    : StreamEncoderWrapper(parent.codec_client_-&gt;newStream(*this)),
      StreamDecoderWrapper(response_decoder), parent_(parent) {
  // 添加回调
  StreamEncoderWrapper::inner_.getStream().addCallbacks(*this);
}

// 底层请求流
StreamEncoder&amp; CodecClient::newStream(StreamDecoder&amp; response_decoder) {
  // response_decoder即UpstreamRequest
  ActiveRequestPtr request(new ActiveRequest(*this, response_decoder));
  // 创建出站请求流
  request-&gt;encoder_ = &amp;codec_-&gt;newStream(*request);
  request-&gt;encoder_-&gt;getStream().addCallbacks(*request);
  request-&gt;moveIntoList(std::move(request), active_requests_);
  disableIdleTimer();
  return *active_requests_.front()-&gt;encoder_;
}

StreamEncoder&amp; ClientConnectionImpl::newStream(StreamDecoder&amp; response_decoder) {
  if (resetStreamCalled()) {
    throw CodecClientException("cannot create new streams after calling reset");
  }
  // 为连接启用读
  while (!connection_.readEnabled()) {
    connection_.readDisable(false);
  }
  request_encoder_ = std::make_unique&lt;RequestStreamEncoderImpl&gt;(*this);
  // 将UpstreamRequest纳入未决响应列表
  pending_responses_.emplace_back(&amp;response_decoder);
  return *request_encoder_;
}</pre>
<p>反之，如果连接池没有空闲HTTP客户端，则创建PendingRequest并排队：</p>
<pre class="crayon-plain-tag">ConnectionPool::Cancellable* ConnPoolImplBase::newPendingRequest(StreamDecoder&amp; decoder, ConnectionPool::Callbacks&amp; callbacks) {
  ENVOY_LOG(debug, "queueing request due to no available connections");
  // 创建PendingRequest
  PendingRequestPtr pending_request(new PendingRequest(*this, decoder, callbacks));
  // 加入pending_requests_列表，然后返回
  pending_request-&gt;moveIntoList(std::move(pending_request), pending_requests_);
  return pending_requests_.front().get();
}</pre>
<p>排队的请求会在以后，因为某种事件而关联到可用连接。例如新的针对上游主机的L4连接建立后：</p>
<pre class="crayon-plain-tag">// Envoy::Http::Http1::ConnPoolImpl::attachRequestToClient conn_pool.cc:66
// Envoy::Http::Http1::ConnPoolImpl::processIdleClient conn_pool.cc:238
  client.stream_wrapper_.reset();
  if (pending_requests_.empty() || delay) {
    // 没有未决请求，将客户端加入空闲列表
    client.moveBetweenLists(busy_clients_, ready_clients_);
  } else {
    // 绑定请求到客户端
    attachRequestToClient(client, pending_requests_.back()-&gt;decoder_, pending_requests_.back()-&gt;callbacks_);
    pending_requests_.pop_back();
  }
// Envoy::Http::Http1::ConnPoolImpl::onConnectionEvent conn_pool.cc:183
  if (event == Network::ConnectionEvent::Connected) {
    conn_connect_ms_-&gt;complete();
    // 有空闲客户端了，处理之
    processIdleClient(client, false);
  }
// Envoy::Http::Http1::ConnPoolImpl::ActiveClient::onEvent conn_pool.h:89
    void onEvent(Network::ConnectionEvent event) override {
      parent_.onConnectionEvent(*this, event);
    }
// Envoy::Network::ConnectionImpl::raiseEvent connection_impl.cc:329
void ConnectionImpl::raiseEvent(ConnectionEvent event) {
  for (ConnectionCallbacks* callback : callbacks_) {
    callback-&gt;onEvent(event);
  }
  if (state() == State::Open &amp;&amp; event == ConnectionEvent::Connected &amp;&amp; write_buffer_-&gt;length() &gt; 0) {
    onWriteReady();
  }
}
// Envoy::Network::RawBufferSocket::onConnected raw_buffer_socket.cc:83
void RawBufferSocket::onConnected() { callbacks_-&gt;raiseEvent(ConnectionEvent::Connected); }
// Envoy::Network::ConnectionImpl::onWriteReady connection_impl.cc:519
    if (error == 0) {
      ENVOY_CONN_LOG(debug, "connected", *this);
      connecting_ = false;
      transport_socket_-&gt;onConnected();
      ...
// Envoy::Network::ConnectionImpl::onFileEvent connection_impl.cc:467</pre>
<p>到这里为止，我们还没搞清楚，针对上游主机的请求到底是何时、由谁发出去的。实际上这是在Router过滤器的onPoolReady回调中进行的。</p>
<div class="blog_h3"><span class="graybg">发送上游请求</span></div>
<p>不管请求是异步还是同步的关联到HTTP客户端（attachRequestToClient），都会触发onPoolReady。此回调会真正发出请求：</p>
<pre class="crayon-plain-tag">void Filter::UpstreamRequest::onPoolReady(Http::StreamEncoder&amp; request_encoder,
                                          Upstream::HostDescriptionConstSharedPtr host) {
  ENVOY_STREAM_LOG(debug, "pool ready", *parent_.callbacks_);

  // 设置UpstreamRequest.upstream_host_ = host
  // 调用UpstreamRequest、Router的StreamInfo.onUpstreamHostSelected()
  onUpstreamHostSelected(host);
  request_encoder.getStream().addCallbacks(*this);

  // 创建per-try的定时器。per_try_timeout_字段被设置为已启用的定时器
  setupPerTryTimeout();
  conn_pool_stream_handle_ = nullptr;
  // 将StreamWrapper设置为请求编码器
  setRequestEncoder(request_encoder);
  calling_encode_headers_ = true;
  if (parent_.route_entry_-&gt;autoHostRewrite() &amp;&amp; !host-&gt;hostname().empty()) {
    // 如果当前路由条目设置了自动头重写，则使用目标上游主机的名称来覆盖请求头
    parent_.downstream_headers_-&gt;Host()-&gt;value(host-&gt;hostname());
  }
  
  // 注入传递当前追踪需要的头
  if (span_ != nullptr) {
    span_-&gt;injectContext(*parent_.downstream_headers_);
  }

  // 日志用途信息
  stream_info_.onFirstUpstreamTxByteSent();
  parent_.callbacks_-&gt;streamInfo().onFirstUpstreamTxByteSent();
  // 进行请求头编码，调用StreamEncoderWrapper，后者装饰一个StreamEncoder的实现RequestStreamEncoderImpl
  request_encoder.encodeHeaders(*parent_.downstream_headers_, !buffered_request_body_ &amp;&amp; encode_complete_ &amp;&amp; !encode_trailers_);
  calling_encode_headers_ = false;

  // 在encodeHeaders()调用过程中可能发生RESET，这里需要进行测试，尽管是非常边缘的情况
  // 例如对于HTTP/2 codec，当帧由于某种原因无法编码的情况下就会出现RESET —— 比如头过大，超过64K
  if (deferred_reset_reason_) {
    // 重置回调
    onResetStream(deferred_reset_reason_.value());
  } else {
    // 编码请求体
    if (buffered_request_body_) {
      stream_info_.addBytesSent(buffered_request_body_-&gt;length());
      request_encoder.encodeData(*buffered_request_body_, encode_complete_ &amp;&amp; !encode_trailers_);
    }
    // 编码请求尾
    if (encode_trailers_) {
      request_encoder.encodeTrailers(*parent_.downstream_trailers_);
    }
    // 记录日志用的流信息
    if (encode_complete_) {
      stream_info_.onLastUpstreamTxByteSent();
      parent_.callbacks_-&gt;streamInfo().onLastUpstreamTxByteSent();
    }
  }
} </pre>
<p>上游请求的编码逻辑如下：</p>
<pre class="crayon-plain-tag">void RequestStreamEncoderImpl::encodeHeaders(const HeaderMap&amp; headers, bool end_stream) {
  // Method、Path头必须存在
  const HeaderEntry* method = headers.Method();
  const HeaderEntry* path = headers.Path();
  if (!method || !path) {
    throw CodecClientException(":method and :path must be specified");
  }
  // 如果是HEAD请求
  if (method-&gt;value() == Headers::get().MethodValues.Head.c_str()) {
    head_request_ = true;
  }
  // 如果是HEAD请求，则设置pending_response.head_request_ = true
  connection_.onEncodeHeaders(headers);
  // 写入报文最前面的部分
  connection_.reserveBuffer(std::max(4096U, path-&gt;value().size() + 4096));
  connection_.copyToBuffer(method-&gt;value().c_str(), method-&gt;value().size());
  connection_.addCharToBuffer(' ');
  connection_.copyToBuffer(path-&gt;value().c_str(), path-&gt;value().size());
  connection_.copyToBuffer(REQUEST_POSTFIX, sizeof(REQUEST_POSTFIX) - 1);
  // 写入请求头部分，包括写入一些额外的头
  StreamEncoderImpl::encodeHeaders(headers, end_stream);
}</pre>
<p>可以看到，<span style="background-color: #c0c0c0;">上游请求的编码，是不走HTTP过滤器链</span>的。 </p>
<div class="blog_h3"><span class="graybg">接收上游响应</span></div>
<p>那么，上游的响应又是如何接收到的呢？在newStream方法调用createNewConnection创建新客户端时，对应的L4连接也会被创建 —— libevent事件回调会被注册：</p>
<pre class="crayon-plain-tag">void ConnPoolImpl::createNewConnection() {
  ActiveClientPtr client(new ActiveClient(*this));
  client-&gt;moveIntoList(std::move(client), busy_clients_);
}
// ActiveClient的构造函数会创建L4连接
ConnPoolImpl::ActiveClient::ActiveClient(ConnPoolImpl&amp; parent)
    : parent_(parent),
      // 连接到服务器端的超时回调
      connect_timer_(parent_.dispatcher_.createTimer([this]() -&gt; void { onConnectTimeout(); })),
      remaining_requests_(parent_.host_-&gt;cluster().maxRequestsPerConnection()) {
  // ...
  // 调用HostImpl.createConnection()
  Upstream::Host::CreateConnectionData data =
      parent_.host_-&gt;createConnection(parent_.dispatcher_, parent_.socket_options_, nullptr);
}

Host::CreateConnectionData HostImpl::createConnection(
    Event::Dispatcher&amp; dispatcher, const Network::ConnectionSocket::OptionsSharedPtr&amp; options,
    Network::TransportSocketOptionsSharedPtr transport_socket_options) const {
  // 创建L4客户端连接
  return {createConnection(dispatcher, *cluster_, address_, options, transport_socket_options),
          shared_from_this()};
}</pre>
<p>响应就是通过libevent回调传递，其<span style="background-color: #c0c0c0;">网络层的处理路径和处理下游请求时是完全一样的——不管是读下游请求还是上游响应，L4过滤器的onData都会被调用</span>，在onContinueReading方法中进行报文的读取。</p>
<p>对于HTTP1来说，当报文头读取完毕后，Http::Http1::ClientConnectionImpl::onHeadersComplete被回调，它会转调PendingResponse.decoder.decodeHeaders方法，后者进而调用UpstreamRequest::decodeHeaders：</p>
<pre class="crayon-plain-tag">void Filter::UpstreamRequest::decodeHeaders(Http::HeaderMapPtr&amp;&amp; headers, bool end_stream) {
  stream_info_.onFirstUpstreamRxByteReceived();
  parent_.callbacks_-&gt;streamInfo().onFirstUpstreamRxByteReceived();
  maybeEndDecode(end_stream);
  // 读取头
  upstream_headers_ = headers.get();
  // 获取响应码
  const uint64_t response_code = Http::Utility::getResponseStatus(*headers);
  stream_info_.response_code_ = static_cast&lt;uint32_t&gt;(response_code);
  // 调用Router
  parent_.onUpstreamHeaders(response_code, std::move(headers), end_stream);
}</pre>
<p>UpstreamRequest.parent_就是Router过滤器，其onUpstreamHeaders的实现如下：</p>
<pre class="crayon-plain-tag">void Filter::onUpstreamHeaders(const uint64_t response_code, Http::HeaderMapPtr&amp;&amp; headers,
                               bool end_stream) {
  ENVOY_STREAM_LOG(debug, "upstream headers complete: end_stream={}", *callbacks_, end_stream);

  // 异常检测信息收集，为上游主机添加一个状态码
  upstream_request_-&gt;upstream_host_-&gt;outlierDetector().putHttpResponseCode(response_code);

  // 健康检查快速失败标记 x-envoy-immediate-health-check-fail，可能通过管理端点设置
  if (headers-&gt;EnvoyImmediateHealthCheckFail() != nullptr) {
    // 设置上游主机健康状态
    upstream_request_-&gt;upstream_host_-&gt;healthChecker().setUnhealthy();
  }

  // 重试相关的处理
  if (retry_state_) {
    // onHostAttempted：当针对一个主机的请求尝试失败了，并且可以进行下一个尝试时，调用此回调
    retry_state_-&gt;onHostAttempted(upstream_request_-&gt;upstream_host_);
    // 判断是否应该进行重试，如果是，调用回调，也就是doRetry()
    RetryStatus retry_status = retry_state_-&gt;shouldRetry(
        headers.get(), absl::optional&lt;Http::StreamResetReason&gt;(), [this]() -&gt; void { doRetry(); });
    // 捕获上游主机，因为后面的setupRetry()调用会清除upstream_request_
    const auto upstream_host = upstream_request_-&gt;upstream_host_;
    if (retry_status == RetryStatus::Yes &amp;&amp; setupRetry(end_stream)) {
      // 重试
      Http::CodeStats&amp; code_stats = httpContext().codeStats();
      code_stats.chargeBasicResponseStat(cluster_-&gt;statsScope(), "retry.", static_cast&lt;Http::Code&gt;(response_code));
      upstream_host-&gt;stats().rq_error_.inc();
      return;
    } else if (retry_status == RetryStatus::NoOverflow) {
      // 上游过载
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    } else if (retry_status == RetryStatus::NoRetryLimitExceeded) {
      // 达到最大重试次数
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamRetryLimitExceeded);
    }

    // 由于end_stream为false时不会调用cleanup()，保证重试定时器被销毁
    retry_state_.reset();
  }
  
  // 处理重定向
  if (static_cast&lt;Http::Code&gt;(response_code) == Http::Code::Found &amp;&amp;
      route_entry_-&gt;internalRedirectAction() == InternalRedirectAction::Handle &amp;&amp; setupRedirect(*headers)) {
    return;
    // If the redirect could not be handled, fail open and let it pass to the
    // next downstream.
  }

  // 处理响应头x-envoy-upstream-service-time
  if (DateUtil::timePointValid(downstream_request_complete_time_)) {
    Event::Dispatcher&amp; dispatcher = callbacks_-&gt;dispatcher();
    MonotonicTime response_received_time = dispatcher.timeSystem().monotonicTime();
    std::chrono::milliseconds ms = std::chrono::duration_cast&lt;std::chrono::milliseconds&gt;(
        response_received_time - downstream_request_complete_time_);
    if (!config_.suppress_envoy_headers_) {
      headers-&gt;insertEnvoyUpstreamServiceTime().value(ms.count());
    }
  }

  // 根据响应头来设置此上游主机是否金丝雀版本
  upstream_request_-&gt;upstream_canary_ =
      (headers-&gt;EnvoyUpstreamCanary() &amp;&amp; headers-&gt;EnvoyUpstreamCanary()-&gt;value() == "true") ||
      upstream_request_-&gt;upstream_host_-&gt;canary();
  chargeUpstreamCode(response_code, *headers, upstream_request_-&gt;upstream_host_, false);

  // 处理非500响应头，主要是进行一些指标的收集
  if (!Http::CodeUtility::is5xx(response_code)) {
    handleNon5xxResponseHeaders(*headers, end_stream);
  }

  // downstream_set_cookies_为需要添加到上游响应头中的Cookies
  for (const auto&amp; header_value : downstream_set_cookies_) {
    headers-&gt;addReferenceKey(Http::Headers::get().SetCookie, header_value);
  }

  // 对响应头进行一系列最后处理：
  // 添加一系列用户定义的响应头，按照顺序： route-action-level、route-level、virtual host level、route-action-level
  route_entry_-&gt;finalizeResponseHeaders(*headers, callbacks_-&gt;streamInfo());

  downstream_response_started_ = true;
  if (end_stream) {
    onUpstreamComplete();
  }

  // 开始向下游发送响应头，这个是要走过滤器链的
  callbacks_-&gt;encodeHeaders(std::move(headers), end_stream);
}</pre>
<p>可以看到，<span style="background-color: #c0c0c0;">上游响应的解码，也是不走HTTP过滤器链</span>的。</p>
<p>另外需要注意，不管是下游请求、上游响应，<span style="background-color: #c0c0c0;">都会经由http_parser回调L7连接的on***Complete方法，不同之处是，对于下游请求来说L7连接的实现是ServerConnectionImpl，而对于上游响应来说L7连接的实现是ClientConnectionImpl</span>。</p>
<p>上游响应头处理完毕后，响应体回调onMessageComplete很快执行：</p>
<pre class="crayon-plain-tag">void ClientConnectionImpl::onMessageComplete() {
  ENVOY_CONN_LOG(trace, "message complete", connection_);
  if (ignore_message_complete_for_100_continue_) {
    ignore_message_complete_for_100_continue_ = false;
    return;
  }
  if (!pending_responses_.empty()) {
    // 取出未决响应，注意这里是HTTP11，每个连接上同时只会有一个未决响应
    PendingResponse response = pending_responses_.front();
    pending_responses_.pop_front();

    if (deferred_end_stream_headers_) {
      // 解码响应头
      response.decoder_-&gt;decodeHeaders(std::move(deferred_end_stream_headers_), true);
      deferred_end_stream_headers_.reset();
    } else {
      // 解码响应体
      Buffer::OwnedImpl buffer;
      response.decoder_-&gt;decodeData(buffer, true);
    }
  }
}</pre>
<p>response.decoder就是UpstreamRequest，其decodeData方法会调用Router过滤器的onUpstreamData，这类似于读取响应头时调用onUpstreamHeaders，类似的、可能被调用的其它回调包括onUpstreamTrailers、onUpstreamMetadata。</p>
<div class="blog_h3"><span class="graybg">处理上游超时 </span></div>
<p>Router过滤器不负责真正的发送上游请求，这是由连接池异步进行的。它调用upstream_request_的encodeHeaders后，<span style="background-color: #c0c0c0;">立即回调onRequestComplete</span>，后者注册了定时器来处理请求超时：</p>
<pre class="crayon-plain-tag">void Filter::onRequestComplete() {
  downstream_end_stream_ = true;
  // 获取事件分发器
  Event::Dispatcher&amp; dispatcher = callbacks_-&gt;dispatcher();
  downstream_request_complete_time_ = dispatcher.timeSystem().monotonicTime();

  // 有可能我们得到一个立即的RESET，因此这里判断上游请求是否为空
  if (upstream_request_) {
    maybeDoShadowing();
    // 如果配置了超时，则注册定时器，回调为onResponseTimeout
    if (timeout_.global_timeout_.count() &gt; 0) {
      response_timeout_ = dispatcher.createTimer([this]() -&gt; void { onResponseTimeout(); });
      response_timeout_-&gt;enableTimer(timeout_.global_timeout_);
    }
  }
}</pre>
<p>如果上游请求超时，下面的函数被调用：</p>
<pre class="crayon-plain-tag">void Filter::onResponseTimeout() {
  ENVOY_STREAM_LOG(debug, "upstream timeout", *callbacks_);
  cluster_-&gt;stats().upstream_rq_timeout_.inc();

  // 可能在执行上游请求重试backoff期间发生超时，那时是没有上游请求的。这种情况下仿冒一个RESET
  if (upstream_request_) {
    if (upstream_request_-&gt;upstream_host_) {
      upstream_request_-&gt;upstream_host_-&gt;stats().rq_timeout_.inc();
    }
    // 请求已经处理，不能取消，必须重置流
    upstream_request_-&gt;resetStream();
  }
  // 触发上游重置，重置的原因有Reset, GlobalTimeout, PerTryTimeout几种，这里是GlobalTimeout
  onUpstreamReset(UpstreamResetType::GlobalTimeout, absl::optional&lt;Http::StreamResetReason&gt;());
}</pre>
<div class="blog_h3"><span class="graybg">处理上游重置</span></div>
<pre class="crayon-plain-tag">void Filter::onUpstreamReset(UpstreamResetType type, const absl::optional&lt;Http::StreamResetReason&gt;&amp; reset_reason) {
  // 全局性超时
  ASSERT(type == UpstreamResetType::GlobalTimeout || upstream_request_);
  // 上游重置
  if (type == UpstreamResetType::Reset) {
    ENVOY_STREAM_LOG(debug, "upstream reset", *callbacks_);
  }

  Upstream::HostDescriptionConstSharedPtr upstream_host;
  if (upstream_request_) {
    upstream_host = upstream_request_-&gt;upstream_host_;
    if (upstream_host) {
      // 为上游主机的断路检测器提供信息，如果是RESET，则记录503，否则记录504（网关超时）
      upstream_host-&gt;outlierDetector().putHttpResponseCode(
          enumToInt(type == UpstreamResetType::Reset ? Http::Code::ServiceUnavailable
                                                     : timeout_response_code_));
    }
  }

  // 全局超时时不会重试，已经开始响应处理后也不会重试
  if (type != UpstreamResetType::GlobalTimeout &amp;&amp; !downstream_response_started_ &amp;&amp; retry_state_) {
    // 回调retry modifiers
    if (upstream_host != nullptr) {
      retry_state_-&gt;onHostAttempted(upstream_host);
    }
    // 判断是否需要重试
    RetryStatus retry_status = retry_state_-&gt;shouldRetry(nullptr, reset_reason, [this]() -&gt; void { doRetry(); });
    if (retry_status == RetryStatus::Yes &amp;&amp; setupRetry(true)) {
      // 需要重试
      if (upstream_host) {
        upstream_host-&gt;stats().rq_error_.inc();
      }
      return;
    // 不应该重试
    } else if (retry_status == RetryStatus::NoOverflow) {
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
    } else if (retry_status == RetryStatus::NoRetryLimitExceeded) {
      callbacks_-&gt;streamInfo().setResponseFlag(
          StreamInfo::ResponseFlag::UpstreamRetryLimitExceeded);
    }
  }

  // 如果尚未向下游发送任何信息，则发送具有适当响应码的响应；否则仅仅是重置响应
  if (downstream_response_started_) {
    if (upstream_request_ != nullptr &amp;&amp; upstream_request_-&gt;grpc_rq_success_deferred_) {
      upstream_request_-&gt;upstream_host_-&gt;stats().rq_error_.inc();
    }
    // 删除重试定时器
    cleanup();
    callbacks_-&gt;resetStream();
  } else {
    cleanup();
    Http::Code code;
    const char* body;
    if (type == UpstreamResetType::GlobalTimeout || type == UpstreamResetType::PerTryTimeout) {
      callbacks_-&gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamRequestTimeout);

      code = timeout_response_code_;
      body = code == Http::Code::GatewayTimeout ? "upstream request timeout" : "";
    } else {
      StreamInfo::ResponseFlag response_flags =
          streamResetReasonToResponseFlag(reset_reason.value());
      callbacks_-&gt;streamInfo().setResponseFlag(response_flags);
      code = Http::Code::ServiceUnavailable;
      body = "upstream connect error or disconnect/reset before headers";
    }

    const bool dropped = reset_reason &amp;&amp; reset_reason.value() == Http::StreamResetReason::Overflow;
    chargeUpstreamCode(code, upstream_host, dropped);
    // 如果有非5xx响应，却仍然被后端重置，或者在响应开始前超时，作为一个错误看待
    if (upstream_host != nullptr &amp;&amp; !Http::CodeUtility::is5xx(enumToInt(code))) {
      upstream_host-&gt;stats().rq_error_.inc();
    }
    // 发送本地响应
    callbacks_-&gt;sendLocalReply(code, body,
                               [dropped, this](Http::HeaderMap&amp; headers) {
                                 if (dropped &amp;&amp; !config_.suppress_envoy_headers_) {
                                   headers.insertEnvoyOverloaded().value(
                                       Http::Headers::get().EnvoyOverloadedValues.True);
                                 }
                               },
                               absl::nullopt);
  }
}</pre>
<div class="blog_h2"><span class="graybg">处理HTTP下游响应写</span></div>
<div class="blog_h3"><span class="graybg">发送本地响应</span></div>
<p>在Envoy的HTTP解码过滤器处理下游请求的过程中，可能由于多种原因（通常是异常情况），立即应答下游客户端，而不向上游转发请求。此时会调用ActiveStreamDecoderFilter，或者直接调用ActiveStream的：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::sendLocalReply(
    bool is_grpc_request, Code code, absl::string_view body,
    const std::function&lt;void(HeaderMap&amp; headers)&gt;&amp; modify_headers, bool is_head_request,
    const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status) {
  // 断言当前流的响应头尚未设置
  ASSERT(response_headers_ == nullptr);
  // 对于过早错误的处理，尽可能尝试创建出过滤器链，以便记录访问日志
  if (!state_.created_filter_chain_) {
    createFilterChain();
  }
  // 调用此工具函数
  Utility::sendLocalReply(is_grpc_request,
                          // 编码响应头的回调
                          [this, modify_headers](HeaderMapPtr&amp;&amp; headers, bool end_stream) -&gt; void {
                            if (modify_headers != nullptr) {
                              // 转发sendLocalReply的入参
                              modify_headers(*headers);
                            }
                            // 移动响应头
                            response_headers_ = std::move(headers);
                            // 编码响应头
                            encodeHeaders(nullptr, *response_headers_, end_stream);
                          },
                          // 编码响应体的回调
                          [this](Buffer::Instance&amp; data, bool end_stream) -&gt; void {
                            // 编码响应体
                            encodeData(nullptr, data, end_stream);
                          },
                          // 被销毁？重置      响应码 响应体 gRPC状态码    提示是否header-only
                          state_.destroyed_, code, body, grpc_status, is_head_request);
}</pre>
<p>上面代码调用的工具函数实现如下：</p>
<pre class="crayon-plain-tag">void Utility::sendLocalReply(
    bool is_grpc, std::function&lt;void(HeaderMapPtr&amp;&amp; headers, bool end_stream)&gt; encode_headers,
    std::function&lt;void(Buffer::Instance&amp; data, bool end_stream)&gt; encode_data, const bool&amp; is_reset,
    Code response_code, absl::string_view body_text,
    const absl::optional&lt;Grpc::Status::GrpcStatus&gt; grpc_status, bool is_head_request) {
  // encode_headers()调用可能重置流，但是在调用它之前，不能是已重置状态
  ASSERT(!is_reset);
  // 如果请求是gRPC，则返回trailers-only的响应
  if (is_grpc) {
    // 处理gRPC协议的响应头
    HeaderMapPtr response_headers{new HeaderMapImpl{
        {Headers::get().Status, std::to_string(enumToInt(Code::OK))},
        {Headers::get().ContentType, Headers::get().ContentTypeValues.Grpc},
        // gRPC状态码作为响应头
        {Headers::get().GrpcStatus,
         std::to_string(
             enumToInt(grpc_status ? grpc_status.value()
                                   : Grpc::Utility::httpToGrpcStatus(enumToInt(response_code))))}}};
    if (!body_text.empty() &amp;&amp; !is_head_request) {
      // 如果提供了响应体，则编码为gRPC消息
      response_headers-&gt;insertGrpcMessage().value(body_text);
    }
    encode_headers(std::move(response_headers), true); // 编码响应头
    return;
  }
  // 处理非gRPC协议的响应头
  HeaderMapPtr response_headers{ new HeaderMapImpl{{Headers::get().Status, std::to_string(enumToInt(response_code))}}};
  if (!body_text.empty()) {
    response_headers-&gt;insertContentLength().value(body_text.size());
    response_headers-&gt;insertContentType().value(Headers::get().ContentTypeValues.Text);
  }

  // 对于header-only响应，编码完头即返回
  if (is_head_request) {
    encode_headers(std::move(response_headers), true);
    return;
  }
  // 否则，如果响应体不为空，则编码头后，再编码体
  encode_headers(std::move(response_headers), body_text.empty());
  // encode_headers()) 调用可能修改了is_reset，因此再次测试：
  if (!body_text.empty() &amp;&amp; !is_reset) {
    // OwnedImpl封装一个分配的evbuffer，evbuffer用于libevent的缓冲网络I/O的缓冲区的处理
    Buffer::OwnedImpl buffer(body_text);
    encode_data(buffer, true);
  }
}</pre>
<div class="blog_h3"><span class="graybg">HTTP响应头编码</span></div>
<p>响应头编码由HTTP连接管理器的ActiveStream::encodeHeaders方法完成：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::encodeHeaders(ActiveStreamEncoderFilter* filter,
                                                        HeaderMap&amp; headers, bool end_stream) {
  // 重置per-stream的空闲定时器，也就是重新计时
  resetIdleTimer();
  // 解除请求超时报警
  disarmRequestTimeout();
  // 设置 state_.local_complete_ = end_stream，并开始迭代过滤器链
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator entry = commonEncodePrefix(filter, end_stream);
  // 在何处开始encodeData()调用
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator continue_data_entry = encoder_filters_.end();

  for (; entry != encoder_filters_.end(); entry++) {
    // 设置过滤器调用状态为正在编码响应头
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::EncodeHeaders));
    state_.filter_call_state_ |= FilterCallState::EncodeHeaders;
    // 设置过滤器的end_stream，如果header-only，或者传入end_stream==true
    // end_stream意味着后面没有响应体需要处理
    (*entry)-&gt;end_stream_ =
        encoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == encoder_filters_.end());
    // 调用过滤器进行编码
    FilterHeadersStatus status = (*entry)-&gt;handle_-&gt;encodeHeaders(headers, (*entry)-&gt;end_stream_);
    // 重置过滤器调用状态
    state_.filter_call_state_ &amp;= ~FilterCallState::EncodeHeaders;
    ENVOY_STREAM_LOG(trace, "encode headers called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 根据上一个过滤器的处理结果决定是否需要继续迭代
    const auto continue_iteration = (*entry)-&gt;commonHandleAfterHeadersCallback(status, encoding_heade刷出rs_only_);

    // 对于header-only应答，标记为local_complete_
    // 这样可以保证不会在doEndStream()中重置下游请求
    if (encoding_headers_only_) {
      state_.local_complete_ = true;
    }

    // 不继续迭代，也不会执行后面的基本响应头
    if (!continue_iteration) {
      return;
    }

    // 这里处理使用header-only响应，但是某个过滤器添加了响应体的情况
    // 不能传递end_stream = true给后续的过滤器
    if (end_stream &amp;&amp; buffered_response_data_ &amp;&amp; continue_data_entry == encoder_filters_.end()) {
      continue_data_entry = entry;
    }
  }

  // 基本响应头
  // 设置Date头
  connection_manager_.config_.dateProvider().setDateHeader(headers);
  // 设置Server头
  // 使用setReference()是安全的，因为serverName()在监听器的生命周期内不会改变
  headers.insertServer().value().setReference(connection_manager_.config_.serverName());
  // 如果是Upgrade请求，且没有响应体，则设置Content-Length头为0
  // 否则，移除Connection头
  // 移除Transfer=Encoding头
  // 如果请求头中设置了Envoy强制跟踪头（x-envoy-force-trace），且存在request-id，则在响应头中设置request-id
  // 移除KeepAlive头
  // 移除ProxyConnection头
  // 根据需要添加内容到Via头
  ConnectionManagerUtility::mutateResponseHeaders(headers, request_headers_.get(), connection_manager_.config_.via());

  // 如果当前应当drain/close连接，在编码响应头块之前发送go away帧
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp;
      // drainClose如果连接应当被drain和close返回true
      // 如果监听器正处于draing状态（原因可能是健康检查、热重启）。此方法的返回值由监听器本地、全局DrainManager共同决定
      // local_drain_manager_-&gt;drainClose() || parent_.server_.drainManager().drainClose()
      connection_manager_.drain_close_.drainClose()) {

    // 对于HTTP/1.1请求来说不做什么实质性的事情，仅仅让L4连接有额外的时间和后续请求竞争
    // 此方法在HTTP/1.1和HTTP/2之间保持逻辑一致
    connection_manager_.startDrainSequence();
    connection_manager_.stats_.named_.downstream_cx_drain_close_.inc();
    ENVOY_STREAM_LOG(debug, "drain closing connection", *this);
  }

  // 由于Connection: Close头，的原因，设置DrainState为Closing
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp; state_.saw_connection_close_) {
    ENVOY_STREAM_LOG(debug, "closing connection due to connection close header", *this);
    connection_manager_.drain_state_ = DrainState::Closing;
  }
  // 由于过载，且配置了在过载后禁用Keepalive，设置DrainState为Closing
  if (connection_manager_.drain_state_ == DrainState::NotDraining &amp;&amp;
      connection_manager_.overload_disable_keepalive_ref_ == Server::OverloadActionState::Active) {
    ENVOY_STREAM_LOG(debug, "disabling keepalive due to envoy overload", *this);
    connection_manager_.drain_state_ = DrainState::Closing;
    connection_manager_.stats_.named_.downstream_cx_overload_disable_keepalive_.inc();
  }

  // 如果准备在对端尚未完成的情况下销毁流，同时连接不支持多路分发（非HTTP2），设置DrainState为Closing
  if (!state_.remote_complete_) {
    if (connection_manager_.codec_-&gt;protocol() != Protocol::Http2) {
      connection_manager_.drain_state_ = DrainState::Closing;
    }

    connection_manager_.stats_.named_.downstream_rq_response_before_rq_complete_.inc();
  }

  // DrainState被置为Closing，且当前不是HTTP2
  if (connection_manager_.drain_state_ == DrainState::Closing &amp;&amp;
      connection_manager_.codec_-&gt;protocol() != Protocol::Http2) {
    // 如果不是Upgrade请求，则设置Connection:Close响应头
    // 关于Connection: close，如果出现在：
    // 1、请求头，表示它希望服务器在发送应答消息后关闭连接
    // 2、响应头，表示服务器会在发送应答消息后关闭连接，如果请求头是Connection: Keep-Alive则同时意味着服务器不支持连接重用
    if (!Utility::isUpgrade(headers)) {
      headers.insertConnection().value().setReference(Headers::get().ConnectionValues.Close);
    }
  }

  // 分布式追踪相关处理
  // 关于x-envoy-decorator-operation头：
  // 1、如果入站请求提供了此头，则应该覆盖在由追踪系统生成的server span中本地定义的operation(span)名
  // 2、如果出站响应存在此头，则应该覆盖任何本地定义的client span的operation(span)名
  if (connection_manager_.config_.tracingConfig()) {
    if (connection_manager_.config_.tracingConfig()-&gt;operation_name_ == Tracing::OperationName::Ingress) {
      // 对于ingress（inbound）响应
      // 如果请求头没有指定x-envoy-decorator-operation，则使用decorator的operation name作为x-envoy-decorator-operation响应头
      if (decorated_operation_) {
        headers.insertEnvoyDecoratorOperation().value(*decorated_operation_);
      }
    } else if (connection_manager_.config_.tracingConfig()-&gt;operation_name_ == Tracing::OperationName::Egress) {
      // 对于egress（outbound）响应
      const HeaderEntry* resp_operation_override = headers.EnvoyDecoratorOperation();
      // 如果已经提供x-envoy-decorator-operation，则覆盖当前Spance的operation值
      if (resp_operation_override) {
        if (!resp_operation_override-&gt;value().empty() &amp;&amp; active_span_) {
          active_span_-&gt;setOperation(resp_operation_override-&gt;value().c_str());
        }
        // 移除x-envoy-decorator-operation头，防止传播给服务
        headers.removeEnvoyDecoratorOperation();
      }
    }
  }

  // 进行统计指标收集
  chargeStats(headers);
  stream_info_.onFirstDownstreamTxByteSent();

  // 现在实际完成基于codec的响应头编码，生成、刷出响应。如果end_stream则endEncode()
  response_encoder_-&gt;encodeHeaders( headers, encoding_headers_only_ || (end_stream &amp;&amp; continue_data_entry == encoder_filters_.end()));
  if (continue_data_entry != encoder_filters_.end()) {
    // 调用之前中止迭代的过滤器的continueEncoding()方法，此方法不会重复调用encodeHeaders()
    // 仿冒的设置stopped_ since=true，原因是continueEncoding()要求如此
    ASSERT(buffered_response_data_);
    (*continue_data_entry)-&gt;stopped_ = true;
    (*continue_data_entry)-&gt;continueEncoding();
  } else {
    // 对于header-only响应 —— 不管是过滤器将其转换为header-only还是上游仅仅返回headers，结束响应编码的处理
    maybeEndEncode(encoding_headers_only_ || end_stream);
  }
}


void ConnectionManagerImpl::ActiveStream::maybeEndEncode(bool end_stream) {
  if (end_stream) {
    // 应当接受响应编码的处理
    // 增加日志信息
    stream_info_.onLastDownstreamTxByteSent();
    // 结束span
    request_response_timespan_-&gt;complete();
    // 处理由于上游响应或者reset导致应当结束的流
    connection_manager_.doEndStream(*this);
  }
}</pre>
<p>每个过滤器的<pre class="crayon-plain-tag">encodeHeaders(Http::HeaderMap&amp; headers, bool)</pre>方法会被调用，返回的Http::FilterHeadersStatus会影响响应头编码的后续处理流程。</p>
<div class="blog_h3"><span class="graybg">HTTP响应体编码</span></div>
<p>如果响应体缓冲区不为空，则需要在编码响应头后，继续处理响应体。响应体缓冲区的内容可能是由上游服务提供，也可能是由某个过滤器写入和修改。</p>
<p>响应体编码由HTTP连接管理器的ActiveStream::encodeData方法完成：</p>
<pre class="crayon-plain-tag">void ConnectionManagerImpl::ActiveStream::encodeData(ActiveStreamEncoderFilter* filter,  Buffer::Instance&amp; data, bool end_stream) {
  // 和编码响应头时一样，重置空闲定时器
  resetIdleTimer();

  // 如果先前已经设置此状态，则直接返回
  if (encoding_headers_only_) {
    return;
  }

  // 产生编码过滤器的列表
  std::list&lt;ActiveStreamEncoderFilterPtr&gt;::iterator entry = commonEncodePrefix(filter, end_stream);
  // 在何处开始encodeTrailers调用
  auto trailers_added_entry = encoder_filters_.end();

  // 是否在响应体编码之前，响应尾已经存在了
  const bool trailers_exists_at_start = response_trailers_ != nullptr;
  for (; entry != encoder_filters_.end(); entry++) {
    // 如果任何一个过滤器的end_stream_被标记，则意味着这个以及后续的过滤器不应该处理数据
    if ((*entry)-&gt;end_stream_) {
      return;
    }
    ASSERT(!(state_.filter_call_state_ &amp; FilterCallState::EncodeData));

    // 设置过滤器调用状态
    state_.filter_call_state_ |= FilterCallState::EncodeData;
    if (end_stream) {
      // 最后一个数据帧      
      state_.filter_call_state_ |= FilterCallState::LastDataFrame;
    }
    // 检查response_trailers_，应对前面的过滤器的encodeData()方法调用addEncodedTrailers()的情况
    // 如果前面的过滤器添加了响应尾，则通知当前、后续过滤器，流处理尚不能结束
    (*entry)-&gt;end_stream_ = end_stream &amp;&amp; !response_trailers_;
    // 调用过滤器进行响应体编码
    FilterDataStatus status = (*entry)-&gt;handle_-&gt;encodeData(data, (*entry)-&gt;end_stream_);
    // 重置过滤器调用状态
    state_.filter_call_state_ &amp;= ~FilterCallState::EncodeData;
    if (end_stream) {
      state_.filter_call_state_ &amp;= ~FilterCallState::LastDataFrame;
    }
    ENVOY_STREAM_LOG(trace, "encode data called: filter={} status={}", *this,
                     static_cast&lt;const void*&gt;((*entry).get()), static_cast&lt;uint64_t&gt;(status));

    // 迭代前没有没有响应尾，但是        现在有响应尾（某过滤器添加）
    if (!trailers_exists_at_start &amp;&amp; response_trailers_ &amp;&amp; trailers_added_entry == encoder_filters_.end()) {
      // 这设置为当前过滤器
      trailers_added_entry = entry;
    }
    // 消息体回调通用处理逻辑
    if (!(*entry)-&gt;commonHandleAfterDataCallback(status, data, state_.encoder_filters_streaming_)) {
      return;
    }
  }

  ENVOY_STREAM_LOG(trace, "encoding data via codec (size={} end_stream={})", *this, data.length(),  end_stream);
  // 日志信息
  stream_info_.addBytesSent(data.length());

  // 如果在encodeData期间添加了响应尾，则需要触发decodeTrailers，让过滤器有机会处理这些尾数据
  if (trailers_added_entry != encoder_filters_.end()) {
    response_encoder_-&gt;encodeData(data, false);
    encodeTrailers(trailers_added_entry-&gt;get(), *response_trailers_);
  } else {
    // 调用StreamEncoder进行实际的响应体写入，并刷出
    response_encoder_-&gt;encodeData(data, end_stream);
    maybeEndEncode(end_stream);
  }
}</pre>
<p>每个过滤器的<pre class="crayon-plain-tag">encodeData(Buffer::Instance&amp;, bool)</pre>方法会被调用，返回的Http::FilterHeadersStatus会影响响应体编码的后续处理流程。 </p>
<div class="blog_h1"><span class="graybg">Envoy发送请求过程</span></div>
<p>Istio使用的不是原版的Envoy，它在项目<a href="https://github.com/istio/proxy">istio/proxy</a>中对Envoy进行了扩展，并在构建时引用Envoy的某个特定Commit Id，构建出完整的、增强的Envoy二进制文件。</p>
<p>Istio对Envoy做的增强主要是引入若干自定义过滤器，Mixer的客户端功能就是依赖于过滤器实现的。 </p>
<div class="blog_h2"><span class="graybg">如何调试</span></div>
<div class="blog_h3"><span class="graybg">启动程序</span></div>
<p>如果要创建完全本地的调试环境，你需要签出两个项目并构建：</p>
<pre class="crayon-plain-tag"># istio
git clone https://github.com/istio/istio.git

# istio/proxy
git clone https://github.com/istio/proxy.git</pre>
<p> 通过上面的项目，启动Pilot Discovery、Pilot Agent、Mixer三个程序。Mixer的启动方式前文已经有说明，Pilot Discovery、Agent的启动方式可以参考<a href="https://blog.gmem.cc/interaction-between-istio-pilot-and-envoy">Istio Pilot与Envoy的交互机制解读</a>一文，需要注意的是，必须把binaryPath参数设置为istio/proxy项目构建出的envoy的路径。</p>
<pre class="crayon-plain-tag">pilot proxy sidecar  ... --binaryPath=/home/alex/CPP/projects/clion/istio/proxy/bazel-bin/src/envoy/envoy</pre>
<p>从istio/proxy构建出envoy时，注意保留调试符号。 </p>
<div class="blog_h3"><span class="graybg">调试Envoy</span></div>
<p>在Pilot Agent启动后，它会产生一个Envoy子进程。你可以用GDB连接到该进程，并在GDB控制台设置源码目录：</p>
<pre class="crayon-plain-tag">directory /home/alex/CPP/lib/libevent/2.1.8-stable
directory /home/alex/CPP/projects/clion/istio/proxy 
directory /home/alex/CPP/projects/clion/istio/proxy/bazel-proxy</pre>
<p>然后暂停程序执行，确保源码路径已经匹配上。 </p>
<div class="blog_h2"><span class="graybg">流量拦截</span></div>
<p>执行下面的命令，获取本地运行的Envoy代理的配置：</p>
<pre class="crayon-plain-tag">curl http://127.0.0.1:15000/config_dump</pre>
<p>可以看到，监听器virtual的端口是15001。假设我们想访问podinfo-canary.default.svc.k8s.gmem.cc在9898端口提供的服务，来了解Envoy代理的行为，可以先设置Iptables规则：</p>
<pre class="crayon-plain-tag"># 针对lo接口的请求不走PREROUTING链
iptables -t nat -A OUTPUT -p tcp -o lo --dport 9898 -j REDIRECT --to-port 15001</pre>
<p>然后，发起请求：</p>
<pre class="crayon-plain-tag">curl -H 'Host: podinfo-canary.default.svc.k8s.gmem.cc' http://127.0.0.1:9898/healthz</pre>
<p>此请求会触发Envoy的处理流程，包括对Mixer的L4、L7过滤器的调用。</p>
<div class="blog_h2"><span class="graybg">Check调用</span></div>
<p>Mixer过滤器处理HTTP请求头的逻辑如下：</p>
<pre class="crayon-plain-tag">FilterHeadersStatus Filter::decodeHeaders(HeaderMap&amp; headers, bool) {
  ENVOY_LOG(debug, "Called Mixer::Filter : {}", __func__);
  request_total_size_ += headers.byteSize();
  // 配置，包含目的服务信息
  ::istio::control::http::Controller::PerRouteConfig config;
  auto route = decoder_callbacks_-&gt;route();
  if (route) {
    ReadPerRouteConfig(route-&gt;routeEntry(), &amp;config);
  }
  // control是每个线程对应一个的控制对象
  // controller是Mixer控制器，以MixerFitlerConfig为参数，执行任务来保证对HTTP/TCP请求的控制
  // RequestHandler handler_，请求处理器，支持对Mixer服务器发起CHECK/REPORT调用
  handler_ = control_.controller()-&gt;CreateRequestHandler(config);

  state_ = Calling;
  initiating_call_ = true;
  // CheckData用于抽取额外的HTTP数据，供Mixer Check使用 —— 它持有HeaderMap、Envoy Metadata、网络连接等信息
  CheckData check_data(headers,
                       decoder_callbacks_-&gt;streamInfo().dynamicMetadata(),
                       decoder_callbacks_-&gt;connection());
  // HeaderUpdate用Istio属性来更新HTTP请求头
  Utils::HeaderUpdate header_update(&amp;headers);
  headers_ = &amp;headers;

  // Check调用相关逻辑：
  // 1、从客户端代理中抽取转发的属性
  // 2、从请求中抽取属性
  // 3、从配置中抽取属性
  // 4、如果有必要，将一部分属性转发给下游
  // 5、执行Check调用
  cancel_check_ = handler_-&gt;Check(
      &amp;check_data, &amp;header_update,
      // TransportCheckFunc 用于异步发起Check调用
      control_.GetCheckTransport(decoder_callbacks_-&gt;activeSpan()),
      // CheckDoneFunc 用于异步调用完成后处理CheckResponse
      [this](const CheckResponseInfo&amp; info) { completeCheck(info); });
  initiating_call_ = false;

  if (state_ == Complete) {
    return FilterHeadersStatus::Continue;
  }
  ENVOY_LOG(debug, "Called Mixer::Filter : {} Stop", __func__);
  return FilterHeadersStatus::StopIteration;
}</pre>
<p>从上面的代码我们可以看到，Mixer过滤器在处理下游请求头期间，会<span style="background-color: #c0c0c0;">异步的发起Check调用</span>：</p>
<pre class="crayon-plain-tag">CancelFunc RequestHandlerImpl::Check(CheckData* check_data,
                                     HeaderUpdate* header_update,
                                     TransportCheckFunc transport,
                                     CheckDoneFunc on_done) {
  // 添加转发的属性
  AddForwardAttributes(check_data);
  // 移除Istio属性 x-istio-attributes
  header_update-&gt;RemoveIstioAttributes();
  // 注入一个包含静态转发属性的头
  service_context_-&gt;InjectForwardedAttributes(header_update);

  if (!service_context_-&gt;enable_mixer_check()) {
    // 如果没有启动Check功能，直接以OK响应回调CheckDoneFunc
    CheckResponseInfo check_response_info;
    check_response_info.response_status = Status::OK;
    on_done(check_response_info);
    return nullptr;
  }

  // 添加Check相关属性
  AddCheckAttributes(check_data);

  // 根据Quota配置添加quota需求
  service_context_-&gt;AddQuotas(&amp;request_context_);
  // 异步发送Check调用
  return service_context_-&gt;client_context()-&gt;SendCheck(transport, on_done,
                                                       &amp;request_context_);
}</pre>
<p>此异步调用完成后，回调：</p>
<pre class="crayon-plain-tag">void Filter::completeCheck(const CheckResponseInfo&amp; info) {
  auto status = info.response_status;
  ENVOY_LOG(debug, "Called Mixer::Filter : check complete {}", status.ToString());
  // 流已经被重置，停止回调
  if (state_ == Responded) {
    return;
  }

  route_directive_ = info.route_directive;

  Utils::CheckResponseInfoToStreamInfo(info, decoder_callbacks_-&gt;streamInfo());

  // 处理来自路由指令的直接响应
  if (route_directive_.direct_response_code() != 0) {
    int status_code = route_directive_.direct_response_code();
    ENVOY_LOG(debug, "Mixer::Filter direct response {}", status_code);
    state_ = Responded;
    decoder_callbacks_-&gt;sendLocalReply(
        Code(status_code), route_directive_.direct_response_body(),
        [this](HeaderMap&amp; headers) {
          UpdateHeaders(headers, route_directive_.response_header_operations());
        },
        absl::nullopt);
    return;
  }

  // 如果状态不是OK，即使没有直接响应，也sendLocalReply
  if (!status.ok()) {
    state_ = Responded;

    int status_code = ::istio::utils::StatusHttpCode(status.error_code());
    decoder_callbacks_-&gt;sendLocalReply(Code(status_code), status.ToString(),
                                       nullptr, absl::nullopt);
    return;
  }

  // 将状态置为完成
  state_ = Complete;

  // 更新请求头
  if (nullptr != headers_) {
    UpdateHeaders(*headers_, route_directive_.request_header_operations());
    headers_ = nullptr;
    if (route_directive_.request_header_operations().size() &gt; 0) {
      decoder_callbacks_-&gt;clearRouteCache();
    }
  }

  if (!initiating_call_) {
    decoder_callbacks_-&gt;continueDecoding();
  }
}</pre>
<div class="blog_h2"><span class="graybg">Report调用</span></div>
<p>Report调用是延迟触发的，Mixer过滤器实现了Envoy::AccessLog::Instance（访问记录器），Report调用作为log方法逻辑的一部分。</p>
<p>Envoy在处理请求之后，可能会延迟的删除一些对象：</p>
<pre class="crayon-plain-tag">DispatcherImpl::DispatcherImpl(Buffer::WatermarkFactoryPtr&amp;&amp; factory, Api::Api&amp; api)
    : deferred_delete_timer_(createTimer([this]() -&gt; void { clearDeferredDeleteList(); })), 
      // 延迟删除定时器</pre>
<p>代表当前请求流的ActiveStream对象就是这样延迟删除的，删除时其析构函数被调用：</p>
<pre class="crayon-plain-tag">ConnectionManagerImpl::ActiveStream::~ActiveStream() {
  // ...
  // 遍历所有日志访问记录器
  for (const AccessLog::InstanceSharedPtr&amp; access_log : connection_manager_.config_.accessLogs()) {
    access_log-&gt;log(request_headers_.get(), response_headers_.get(), response_trailers_.get(),
                    stream_info_);
  }
  // ...
}</pre>
<p>可以看到，在ActiveStream析构时会调用所有访问日志记录器，包括Envoy::Http::Mixer::Filter::log：</p>
<pre class="crayon-plain-tag">void Filter::log(const HeaderMap* request_headers,
                 const HeaderMap* response_headers,
                 const HeaderMap* response_trailers,
                 const StreamInfo::StreamInfo&amp; stream_info) {
  ENVOY_LOG(debug, "Called Mixer::Filter : {}", __func__);
  if (!handler_) {
    if (request_headers == nullptr) {
      return;
    }
    // 可能因为请求被其它过滤器拒绝，Mixer过滤器没调用，因此handler尚未初始化
    ::istio::control::http::Controller::PerRouteConfig config;
    ReadPerRouteConfig(stream_info.routeEntry(), &amp;config);
    handler_ = control_.controller()-&gt;CreateRequestHandler(config);
  }

  // 如果没有调用check，则check属性没被抽取
  CheckData check_data(*request_headers, stream_info.dynamicMetadata(), decoder_callbacks_-&gt;connection());
  // ReportData提供接口，抽取HTTP属性，供Mixer Report调用使用
  ReportData report_data(response_headers, response_trailers, stream_info, request_total_size_);
  handler_-&gt;Report(&amp;check_data, &amp;report_data);
}</pre>
<p>Report方法的实现如下：</p>
<pre class="crayon-plain-tag">void RequestHandlerImpl::Report(CheckData* check_data,
                                ReportData* report_data) {
  if (!service_context_-&gt;enable_mixer_report()) {
    return;
  }

  // 添加转发的属性
  AddForwardAttributes(check_data);
  // 添加Check属性
  AddCheckAttributes(check_data);

  AttributesBuilder builder(&amp;request_context_);
  // 抽取Report属性
  builder.ExtractReportAttributes(report_data);

  // 发送Report请求
  service_context_-&gt;client_context()-&gt;SendReport(request_context_);
}</pre>
<p>发送Report请求的工作最终委托给::istio::mixerclient::MixerClient：</p>
<pre class="crayon-plain-tag">void ClientContextBase::SendReport(const RequestContext&amp; request) {
  mixer_client_-&gt;Report(*request.attributes);
}</pre>
<p>MixerClient包含批量处理的逻辑：</p>
<pre class="crayon-plain-tag">void MixerClientImpl::Report(const Attributes &amp;attributes) {
  report_batch_-&gt;Report(attributes);
}

void ReportBatch::Report(const Attributes&amp; request) {
  std::lock_guard&lt;std::mutex&gt; lock(mutex_);
  ++total_report_calls_;
  // 添加请求、压缩
  batch_compressor_-&gt;Add(request);
  // 如果超过批量限制，立即Report
  if (batch_compressor_-&gt;size() &gt;= options_.max_batch_entries) {
    FlushWithLock();
  } else {
    // 否则，延迟发送
    if (batch_compressor_-&gt;size() == 1 &amp;&amp; timer_create_) {
      if (!timer_) {
        timer_ = timer_create_([this]() { Flush(); });
      }
      timer_-&gt;Start(options_.max_batch_time_ms);
    }
  }
} </pre>
<div class="blog_h2"><span class="graybg">属性抽取 </span></div>
<p>MixerClient通过rRPC向Mixer服务器发送的是属性（Attributes），过滤器在调用MixerClient之前，会进行属性的抽取。</p>
<div class="blog_h3"><span class="graybg">Report属性 </span></div>
<pre class="crayon-plain-tag">void AttributesBuilder::ExtractReportAttributes(ReportData *report_data) {
  utils::AttributesBuilder builder(request_-&gt;attributes);

  std::string dest_ip;
  int dest_port;
  // 抽取 destination.ip
  if (report_data-&gt;GetDestinationIpPort(&amp;dest_ip, &amp;dest_port)) {
    if (!builder.HasAttribute(utils::AttributeName::kDestinationIp)) {
      builder.AddBytes(utils::AttributeName::kDestinationIp, dest_ip);
    }
    if (!builder.HasAttribute(utils::AttributeName::kDestinationPort)) {
      builder.AddInt64(utils::AttributeName::kDestinationPort, dest_port);
    }
  }

  std::string uid;
  // 抽取 destination.uid
  if (report_data-&gt;GetDestinationUID(&amp;uid)) {
    builder.AddString(utils::AttributeName::kDestinationUID, uid);
  }

  // 抽取 response.headers  所有响应头作为一个属性
  std::map&lt;std::string, std::string&gt; headers = report_data-&gt;GetResponseHeaders();
  builder.AddStringMap(utils::AttributeName::kResponseHeaders, headers);

  // 抽取 response.time
  builder.AddTimestamp(utils::AttributeName::kResponseTime, std::chrono::system_clock::now());

  ReportData::ReportInfo info;
  report_data-&gt;GetReportInfo(&amp;info);
  // 抽取 request.size
  builder.AddInt64(utils::AttributeName::kRequestBodySize, info.request_body_size);
  // 抽取 response.size
  builder.AddInt64(utils::AttributeName::kResponseBodySize, info.response_body_size);
  // 抽取 request.total_size
  builder.AddInt64(utils::AttributeName::kRequestTotalSize, info.request_total_size);
  // 抽取 response.total_size
  builder.AddInt64(utils::AttributeName::kResponseTotalSize, info.response_total_size);
  // 抽取 response.duration
  builder.AddDuration(utils::AttributeName::kResponseDuration, info.duration);

  // 抽取check属性
  if (!request_-&gt;check_status.ok()) {
    // 抽取 response.code
    builder.AddInt64(utils::AttributeName::kResponseCode, utils::StatusHttpCode(request_-&gt;check_status.error_code()));
    // 抽取 check.error_code
    builder.AddInt64(utils::AttributeName::kCheckErrorCode, request_-&gt;check_status.error_code());
    // 抽取 check.error_message
    builder.AddString(utils::AttributeName::kCheckErrorMessage, request_-&gt;check_status.ToString());
  } else {
    builder.AddInt64(utils::AttributeName::kResponseCode, info.response_code);
  }

  ReportData::GrpcStatus grpc_status;
  if (report_data-&gt;GetGrpcStatus(&amp;grpc_status)) {
    // 抽取 response.grpc_status
    builder.AddString(utils::AttributeName::kResponseGrpcStatus,  grpc_status.status);
    // 抽取 response.grpc_message
    builder.AddString(utils::AttributeName::kResponseGrpcMessage, grpc_status.message);
  }

  builder.AddString(utils::AttributeName::kContextProxyErrorCode, info.response_flags);

  ReportData::RbacReportInfo rbac_info;
  if (report_data-&gt;GetRbacReportInfo(&amp;rbac_info)) {
    if (!rbac_info.permissive_resp_code.empty()) {
      // 抽取 context.proxy_error_code
      builder.AddString(utils::AttributeName::kRbacPermissiveResponseCode, rbac_info.permissive_resp_code);
    }
    if (!rbac_info.permissive_policy_id.empty()) {
      // 抽取 rbac.permissive.effective_policy_id"
      builder.AddString(utils::AttributeName::kRbacPermissivePolicyId, rbac_info.permissive_policy_id);
    }
  }

  builder.FlattenMapOfStringToStruct(report_data-&gt;GetDynamicFilterState());
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy">Istio Mixer与Envoy的交互机制解读</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/interaction-between-istio-mixer-and-envoy/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
