Wednesday, December 22, 2021

SegmentFault 最新的文章

SegmentFault 最新的文章


声临其境,轻松几步教你把音频变成3D环绕音

Posted: 14 Dec 2021 12:23 AM PST

在音乐创作、音视频剪辑和游戏等领域中,给用户带来沉浸式音频体验越来越重要。开发者如何在应用内打造3D环绕声效?华为音频编辑服务6.2.0版本此次带来了空间动态渲染功能,可以将人声、乐器等音频元素渲染到指定的三维空间方位,支持静态和动态渲染两种模式,进一步提升应用中的音效体验。开发者可以点击查看以下Demo演示,了解集成效果并上手实验功能特性。

开发实战

1. 开发准备

开发者提前准备音乐素材,MP3格式最佳。其他音频格式请参考"2.4"步骤转换,视频格式请参考"2.5"步骤进行音频提取。

1.1项目级build.gradle里配置Maven仓地址

buildscript {     repositories {         google()         jcenter()         // 配置HMS Core SDK的Maven仓地址。         maven {url 'https://developer.huawei.com/repo/'}     }     dependencies {         ...         // 增加agcp插件配置。         classpath 'com.huawei.agconnect:agcp:1.4.2.300'     } } allprojects {     repositories {         google()         jcenter()         // 配置HMS Core SDK的Maven仓地址。         maven {url 'https://developer.huawei.com/repo/'}     } } 

1.2 文件头增加配置

apply plugin: 'com.huawei.agconnect'

1.3 应用级build.gradle里配置SDK依赖

dependencies{     implementation 'com.huawei.hms:audio-editor-ui:{version}' }

1.4在AndroidManifest.xml文件中申请如下权限

<!--震动权限--> <uses-permission android:name="android.permission.VIBRATE" /> <!--麦克风权限--> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!--写存储权限--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!--读存储权限--> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!--网络权限--> <uses-permission android:name="android.permission.INTERNET" /> <!--网络状态权限--> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!--网络状态变化权限--> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />

2.代码开发

2.1创建应用自定义的activity界面,用于选择音频,并将该音频文件路径返回给音频编辑SDK

// 将音频文件路径List返回到音频编辑页面 private void sendAudioToSdk() {     // 获取到的音频文件路径 filePath     String filePath = "/sdcard/AudioEdit/audio/music.aac";     ArrayList<String> audioList = new ArrayList<>();     audioList.add(filePath);     // 将音频文件路径返回到音频编辑页面     Intent intent = new Intent();     // 使用sdk提供的HAEConstant.AUDIO_PATH_LIST     intent.putExtra(HAEConstant.AUDIO_PATH_LIST, audioList);     // 使用sdk提供的HAEConstant.RESULT_CODE为结果CODE     this.setResult(HAEConstant.RESULT_CODE, intent);     finish(); }

2.2在UI界面导入音频时,SDK会发送一个action值为com.huawei.hms.audioeditor.chooseaudio的intent以跳转到该activity。因此,该activity"AndroidManifest.xml"中的注册形式如下

<activity android:name="Activity ">  <intent-filter>  <action android:name="com.huawei.hms.audioeditor.chooseaudio"/>  <category android:name="android.intent.category.DEFAULT"/>  </intent-filter>  </activity>

2.3启动音频编辑页面,点击"添加音频",SDK会主动调用"2.1"步骤中定义的activity。添加好音频,就可以进行音频编辑、特效添加等操作,完成后导出编辑音频

HAEUIManager.getInstance().launchEditorActivity(this);

2.4.如果音频素材不是MP3格式,此步骤可以完成音频格式转换

调用transformAudioUseDefaultPath接口进行音频格式转换,转换后的音频文件导出到默认路径。

// 音频格式转换接口 HAEAudioExpansion.getInstance().transformAudioUseDefaultPath(context,inAudioPath, audioFormat, new OnTransformCallBack() {     // 进度回调(0-100)     @Override     public void onProgress(int progress) {     }     // 转换失败     @Override     public void onFail(int errorCode) {     }     // 转换成功     @Override     public void onSuccess(String outPutPath) {     }     // 取消转换     @Override     public void onCancel() {     }     });  // 取消转换任务接口 HAEAudioExpansion.getInstance().cancelTransformAudio();

调用transformAudio接口进行音频格式转换,转换后的音频文件导出到目标路径。

// 音频格式转换接口 HAEAudioExpansion.getInstance().transformAudio(context,inAudioPath, outAudioPath, new OnTransformCallBack(){     // 进度回调(0-100)     @Override     public void onProgress(int progress) {     }     // 转换失败     @Override     public void onFail(int errorCode) {     }     // 转换成功     @Override     public void onSuccess(String outPutPath) {     }     // 取消转换     @Override     public void onCancel() {     }     }); // 取消转换任务接口 HAEAudioExpansion.getInstance().cancelTransformAudio();

2.5如果素材是视频格式,可以调用extractAudio接口进行音频提取,从视频中提取音频文件再导出到指定目录

// outAudioDir提取出的音频保存的文件夹路径,非必填 // outAudioName提取出的音频名称,不带后缀,非必填 HAEAudioExpansion.getInstance().extractAudio(context,inVideoPath,outAudioDir, outAudioName,new AudioExtractCallBack() {     @Override     public void onSuccess(String audioPath) {     Log.d(TAG, "ExtractAudio onSuccess : " + audioPath);     }     @Override     public void onProgress(int progress) {     Log.d(TAG, "ExtractAudio onProgress : " + progress);     }     @Override     public void onFail(int errCode) {     Log.i(TAG, "ExtractAudio onFail : " + errCode);     }     @Override     public void onCancel() {     Log.d(TAG, "ExtractAudio onCancel.");     }     }); // 取消音频提取任务接口 HAEAudioExpansion.getInstance().cancelExtractAudio();

2.6调用getInstruments和startSeparationTasks接口进行伴奏提取

// 获取提取伴奏类型ID,后面将此ID传给接口 HAEAudioSeparationFile haeAudioSeparationFile = new HAEAudioSeparationFile(); haeAudioSeparationFile.getInstruments(new SeparationCloudCallBack<List<SeparationBean>>() {     @Override public void onFinish(List<SeparationBean> response) { // 返回的数据,包括伴奏的类型ID }     @Override     public void onError(int errorCode) {         // 失败返回 } }); // 设置要提取的伴奏参数 List instruments = new ArrayList<>(); instruments.add("伴奏id"); haeAudioSeparationFile.setInstruments(instruments); // 开始进行伴奏分离 haeAudioSeparationFile.startSeparationTasks(inAudioPath, outAudioDir, outAudioName, new AudioSeparationCallBack() {     @Override     public void onResult(SeparationBean separationBean) { }     @Override     public void onFinish(List<SeparationBean> separationBeans) {}     @Override     public void onFail(int errorCode) {}     @Override     public void onCancel() {} }); // 取消分离任务 haeAudioSeparationFile.cancel();

2.7调用applyAudioFile接口进行空间方位渲染

// 空间方位渲染 // 固定摆位 HAESpaceRenderFile haeSpaceRenderFile = new HAESpaceRenderFile(SpaceRenderMode.POSITION); haeSpaceRenderFile.setSpacePositionParams(                             new SpaceRenderPositionParams(x, y, z)); // 动态渲染 HAESpaceRenderFile haeSpaceRenderFile = new HAESpaceRenderFile(SpaceRenderMode.ROTATION); haeSpaceRenderFile.setRotationParams( new SpaceRenderRotationParams(                                     x, y, z, surroundTime, surroundDirection)); // 扩展 HAESpaceRenderFile haeSpaceRenderFile = new HAESpaceRenderFile(SpaceRenderMode.EXTENSION); haeSpaceRenderFile.setExtensionParams(new SpaceRenderExtensionParams(radiusVal, angledVal)); // 调用接口 haeSpaceRenderFile.applyAudioFile(inAudioPath, outAudioDir, outAudioName, callBack); // 取消空间方位渲染 haeSpaceRenderFile.cancel();

完成以上步骤,就可以得到对应的空间动态渲染效果,在应用内轻松实现2D转3D音效啦!这项功能还可以应用到企业会议以及运动康复领域,比如在展会上进行产品沉浸式展示、作为视障人群的方向感线索,为日常生活提供便利等。开发者们可以根据自己应用的实际需求选择使用,如需了解更多详情,请参考:
华为开发者联盟音频编辑服务官网; 获取集成音频编辑服务指导文档

了解更多详情>>

访问华为开发者联盟官网
获取开发指导文档
华为移动服务开源仓库地址:GitHubGitee

关注我们,第一时间了解 HMS Core 最新技术资讯~

【建议收藏】11+实战技巧,让你轻松从Vue过渡到React

Posted: 21 Dec 2021 08:56 AM PST

前言

在这个卷神辈出的时代,只是熟练Vue的胖头鱼,已经被毒打过多次了,面试中曾被质疑:"你居然不会React?"我无语凝噎,不知说啥是好。

这篇文章尝试将Vue中一些常见的功能在React中实现一遍,如果你恰巧是VueReact,或者ReactVue,期待对你有些帮助。

如果你是一名熟悉ReactVue的同学跪求轻喷(手动求生)

每个功能,都有对应的Vue和React版本实现,也有对应的截图或者录屏

Vue仓库

React仓库

1. v-if

我们先从最常见的显示隐藏开始,Vue中处理一个元素的显示隐藏一般会用v-if或者v-show指令,只不过v-if是"真正"的条件渲染,切换过程中条件块内的事件监听器和子组件会适当地被销毁和重建。而v-show就简单了,只是css样式上的控制。

v-if源代码点这里

Vue

<template>   <div class="v-if">     <button @click="onToggleShow">切换</button>     <div v-if="isShow">前端胖头鱼 显示出来啦</div>   </div> </template>  <script> export default {   name: 'vif',   data () {     return {       isShow: true     }   },   methods: {     onToggleShow () {       this.isShow = !this.isShow     }   } } </script>

React

vif源代码点这里

import React, { useState } from "react"  export default function Vif (){   const [ isShow, setIsShow ] = useState(true)   const onToggleShow = () => {     setIsShow(!isShow)   }    return (     <div className="v-if">       <button onClick={ onToggleShow }>切换</button>       {/* 也可以用三目运算符 */}       {/* { isShow ? <div>前端胖头鱼 显示出来啦</div> : null } */}       {         isShow && <div>前端胖头鱼 显示出来啦</div>       }     </div>   ) }

预览

2. v-show

同上,这次我们通过v-show来实现显示隐藏的功能,同时观察DOM的样式变化

注意: 这里为啥显示的时候不设置为block是因为有些元素本身不是块级元素,如果强行设置为block有可能导致错误的样式。

Vue

v-show源代码点击这里

<template>   <div class="v-show">     <button @click="onToggleShow">切换</button>     <div v-show="isShow">前端胖头鱼 显示出来啦</div>   </div> </template>  <script> export default {   name: 'vshow',   data () {     return {       isShow: true     }   },   methods: {     onToggleShow () {       this.isShow = !this.isShow     }   } } </script>  

React

vShow源代码点这里

import React, { useState } from "react"  export default function VShow (){   const [ isShow, setIsShow ] = useState(true)   const onToggleShow = () => {     setIsShow(!isShow)   }    return (     <div className="v-show">       <button onClick={ onToggleShow }>切换</button>       {         <div style={{ display: isShow ? '' : 'none' }}>前端胖头鱼 显示出来啦</div>       }     </div>   ) }  

预览

3. v-for

一般情况下,渲染一个列表在Vue中使用v-for指令,v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。当然了,每个元素都需要设置唯一的key

Vue

v-for源代码点这里

<template>   <div class="v-for">     <div        class="v-for-item"       v-for="item in list"       :key="item.id"     >       {{ item.name }}     </div>   </div> </template>  <script> export default {   name: 'vfor',   data () {     return {       list: [         {           id: 1,           name: '前端',         },         {           id: 2,           name: '后端',         },         {           id: 3,           name: 'android',         },         {           id: 4,           name: 'ios',         },       ]     }   } } </script> 

React

React没有v-for指令,我们可以采用map遍历的方式实现类似功能

vFor源代码点这里

import React, { useState } from "react"  export default function VFor (){   const [ list, setList ] = useState([     {       id: 1,       name: '前端',     },     {       id: 2,       name: '后端',     },     {       id: 3,       name: 'android',     },     {       id: 4,       name: 'ios',     },   ])    return (     <div className="v-for">       {         list.map((item) => {           return <div className="v-for-item" key={ item.id }>{ item.name }</div>         })       }     </div>   ) }  

预览

v-for.png

4. computed

当某个变量需要依赖其他变量求值时,使用计算属性会非常方便,并且Vue的计算属性是基于它们的响应式依赖进行缓存的,依赖值未发生变化,不会重新计算,达到缓存的作用。

我们来看一个简单的加法例子:num3num1num2相加所得,同时按钮每点一次num1加10,num3也会跟着不断加10

Vue

computed源代码点这里

<template>   <div class="computed">     <button @click="onAdd">+10</button>     <div>计算结果:{{ num3 }}</div>   </div> </template>  <script> export default {   name: 'computed',   data () {     return {       num1: 10,       num2: 10,     }   },   computed: {     num3 () {       return this.num1 + this.num2     }   },   methods: {     onAdd () {       this.num1 += 10     }   } } </script> 

React

React没有计算属性,但是我们可以通过useMemo这个hook来实现,和Vue computed不太一样的地方在于,我们必须手动维护依赖

computed源代码点这里

import React, { useMemo, useState } from "react"  export default function Computed (){   const [ num1, setNum1 ] = useState(10)   const [ num2, setNum2 ] = useState(10)    const num3 = useMemo((a, b) => {     return num1 + num2   }, [ num1, num2 ])    const onAdd = () => {     setNum1(num1 + 10)   }    return (     <div className="computed">       <button onClick={ onAdd }>+10</button>       <div>计算结果:{ num3 }</div>     </div>   ) }  

预览

computed.gif

5. watch

有时候我们需要监听数据变化然后执行异步行为或者开销较大的操作时,在Vue中可以使用watch来实现

我们来模拟一个这样的场景并且通过watch来实现:选择boy或者girl,选中后发送请求,显示请求结果。(这里通过setTimeout模拟异步请求过程)

Vue

watch源代码点这里

<template>   <div class="watch">     <div class="selects">       <button          v-for="(item, i) in selects"         :key="i"         @click="onSelect(item)"       >         {{ item }}       </button>     </div>     <div class="result">       {{ result }}     </div>   </div> </template>  <script> export default {   name: 'watch',   data () {     return {       fetching: false,       selects: [         'boy',         'girl'       ],       selectValue: ''     }   },   computed: {     result () {       return this.fetching ? '请求中' : `请求结果: 选中${this.selectValue || '~'}`     }   },   watch: {     selectValue () {       this.fetch()     }   },   methods: {     onSelect (value) {       this.selectValue = value       },     fetch () {       if (!this.fetching) {         this.fetching = true          setTimeout(() => {           this.fetching = false         }, 1000)       }     }   } } </script>

React

React中要实现监听某些数据的变化执行响应的动作,可以使用useEffect

watch源代码点这里

import React, { useState, useMemo, useEffect } from "react" import './watch.css'  export default function Watch() {   const [fetching, setFetching] = useState(false)   const [selects, setSelects] = useState([     'boy',     'girl'   ])   const [selectValue, setSelectValue] = useState('')    const result = useMemo(() => {     return fetching ? '请求中' : `请求结果: 选中${selectValue || '~'}`   }, [ fetching ])    const onSelect = (value) => {     setSelectValue(value)   }   const fetch = () => {     if (!fetching) {       setFetching(true)        setTimeout(() => {         setFetching(false)       }, 1000)     }   }    useEffect(() => {     fetch()   }, [ selectValue ])    return (     <div className="watch">       <div className="selects">         {           selects.map((item, i) => {             return <button key={ i } onClick={ () => onSelect(item) }>{ item }</button>           })         }       </div>       <div className="result">         { result }       </div>     </div>   ) }  

预览

watch.gif

6. style

有时候难免要给元素动态添加样式styleVueReact都给我们提供了方便的使用方式。

在使用上基本大同小异:

相同点:

CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名

不同点:

  1. Vue可以通过数组语法绑定多个样式对象,React主要是单个对象的形式(这点Vue也可以)
  2. React 会自动添加 "px"(这点Vue不会自动处理) 后缀到内联样式为数字的属性,其他单位手动需要手动指定
  3. React样式不会自动补齐前缀。如需支持旧版浏览器,需手动补充对应的样式属性。Vue中当 v-bind:style 使用需要添加浏览器引擎前缀的 CSS property 时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

Vue

style源代码点这里

<template>   <div class="style" :style="[ style, style2 ]"></div> </template>  <script> export default {   name: 'style',   data () {     return {       style: {         width: '100%',         height: '500px',       },       style2: {         backgroundImage: 'linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)',         borderRadius: '10px',       }     }   } } </script>

React

style源代码点这里

import React from "react"  export default function Style (){   const style = {     width: '100%',     height: '500px',   }   const style2 = {     backgroundImage: 'linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)',     borderRadius: '10px',   }    return (     <div className="style" style={ { ...style, ...style2 } } ></div>   ) } 

预览

7. class

如何动态地给元素添加class? Vue中我自己比较喜欢用数组的语法(当然还有对象的写法),React中也可以使用一些第三方包如classnames起到更加便捷添加class的效果。

下面我们看下不借助任何库,如何实现按钮选中的效果

Vue

class源代码点这里

<template>   <button :class="buttonClasses" @click="onClickActive">{{ buttonText }}</button> </template>  <script> export default {   name: 'class',   data () {     return {       isActive: false,     }   },   computed: {     buttonText () {       return this.isActive ? '已选中' : '未选中'     },     buttonClasses () {       // 通过数组形式维护class动态列表                return [ 'button', this.isActive ? 'active' : '' ]     }   },   methods: {     onClickActive () {       this.isActive = !this.isActive     }   } } </script>  <style scoped> .button{   display: block;   width: 100px;   height: 30px;   line-height: 30px;   border-radius: 6px;   margin: 0 auto;   padding: 0;   border: none;   text-align: center;   background-color: #efefef; }  .active{   background-image: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);   color: #fff }  </style>

React

class源代码点这里

import React, { useMemo, useState } from "react"  import './class.css' // 此处样式与上面是一样的   export default function Class (){   const [ isActive, setIsActive ] = useState(false)   const buttonText = useMemo(() => {     return isActive ? '已选中' : '未选中'   }, [ isActive ])   const buttonClass = useMemo(() => {     // 和Vue中不太一样的是我们需要手动join一下,变成'button active'形式     return [ 'button', isActive ? 'active' : '' ].join(' ')   }, [ isActive ])    const onClickActive = () => {     setIsActive(!isActive)   }    return (     <div className={ buttonClass } onClick={onClickActive}>{ buttonText }</div>   ) } 

预览

class.gif

8.provide/inject

Vue和React中对于全局状态的管理都有各自好的解决方案,比如Vue中的Vuex,React中的reduxMobx,当然小型项目中引入这些有点大材小用了,有没有其他解决方案呢?

Vue中可以使用provide/inject

React中则可以使用Context

假设全局有有一个用户信息userInfo的变量,需要在各个组件中都能便捷的访问到,在Vue和React中该如何实现呢?

Vue

Vue中借用provide/inject可以将顶层状态,传递至任意子节点,假设我们再app.vue中声明了一个userInfo数据

provide源代码点这里

app.vue

 <template>   <div id="app">     <div class="title">我是Vue栗子</div>     <router-view/>   </div> </template> <script>  export default {   name: 'app',   // 声明数据       provide () {     return {       userInfo: {         name: '前端胖头鱼'       }     }   } } </script>

provide.vue

<template>   <div class="provide-inject">{{ userInfo.name }}</div> </template>  <script> export default {   name: 'provideInject',   // 使用数据   inject: [ 'userInfo' ] } </script> 

React

React中要实现类似的功能,可以借助Context,将全局状态共享给任意子节点

provide源代码点这里

context/index.js

import { createContext } from "react";  export const UserInfoContext = createContext({   userInfo: {     name: ''   } })

app.js

import { UserInfoContext } from './context/index'  function App() {   return (     <BrowserRouter>       // 注意这里       <UserInfoContext.Provider         value={{ userInfo: { name: '前端胖头鱼' } }}       >         <div className="title">我是React栗子</div>         <Routes>           <Route path="/v-if" element={<Vif />} />           <Route path="/v-show" element={<VShow />} />           <Route path="/v-for" element={<VFor />} />           <Route path="/computed" element={<Computed />} />           <Route path="/watch" element={<Watch />} />           <Route path="/style" element={<Style />} />           <Route path="/class" element={<Class />} />           <Route path="/slot" element={<Slot />} />           <Route path="/nameSlot" element={<NameSlot />} />           <Route path="/scopeSlot" element={<ScopeSlot />} />           <Route path="/provide" element={<Provide />} />         </Routes>       </UserInfoContext.Provider>     </BrowserRouter>   ); }

provide.js

import React, { useContext } from "react" import { UserInfoContext } from '../context/index'   export default function Provide() {   // 通过userContext,使用定义好的UserInfoContext   const { userInfo } = useContext(UserInfoContext)    return (     <div class="provide-inject">{ userInfo.name }</div>   ) }  

预览

image.png

9. slot(默认插槽)

插槽是Vue中非常实用的功能,我把他理解成"坑位",等待着你从外面把他填上,而这个"坑位"可以分成默认坑位具名坑位作用域坑位,咱们通过一个实战例子来看看React中如何实现同等的功能。

假设我们要实现一个简单的dialog组件,基本功能是标题可以传字符串,内容部分可以完全自定义,应该怎么实现呢?

image.png

Vue

slot源代码点这里

dialog

<template>   <div class="dialog" v-show="visible">     <div class="dialog-mask" @click="onHide"></div>     <div class="dialog-body">       <div class="dialog-title" v-if="title">{{ title }}</div>       <div class="dialog-main">         // 注意这里放了一个默认插槽坑位         <slot></slot>       </div>       <div class="dialog-footer">         <div class="button-cancel" @click="onHide">取消</div>         <div class="button-confirm" @click="onHide">确定</div>       </div>     </div>   </div> </template>  <script> export default {   name: "dialog",   props: {     title: {       type: String,       default: "",     },     visible: {       type: Boolean,       default: false,     },   },   methods: {     onHide () {       this.$emit('update:visible', false)     }   } }; </script> 

slot

<template>   <div class="slot">     <button @click="onToggleVisible">切换dialog</button>     <Dialog       :visible.sync="visible"       title="默认插槽"     >       // 这里会替换到<slot></slot>的位置处       <div class="slot-body">前端胖头鱼</div>     </Dialog>   </div> </template>  <script> import Dialog from './components/dialog.vue'  export default {   name: 'slot',   components: {     Dialog,   },   data () {     return {       visible: false     }   },   methods: {     onToggleVisible () {       this.visible = !this.visible     }   } } 

React

要在React中同样实现上面的功能应该怎么办呢?React可没有啥插槽啊!别急,虽然React中没有插槽的概念,但是却可以通过props.children获取到组件内部的子元素,通过这个就可以实现默认插槽的功能

slot源代码点这里

Dialog

import React, { useState, useEffect } from "react"  import './dialog.css'  export default function Dialog(props) {   // 原谅我用visible -1这种傻叉的方式先实现了, 重点不是在这里   const { children, title = '', visible = -1 } = props   const [visibleInner, setVisibleInner] = useState(false)    const onHide = () => {     setVisibleInner(false)   }    useEffect(() => {     setVisibleInner(visible > 0)   }, [ visible ])    return (     <div className="dialog" style={ { display: visibleInner ? 'block' : 'none' }}>       <div className="dialog-mask" onClick={ onHide }></div>       <div className="dialog-body">         { title ? <div className="dialog-title">{ title }</div> : null }         <div className="dialog-main">           {/* 注意这里,通过children实现默认插槽功能 */}           {children}         </div>         <div className="dialog-footer">           <div className="button-cancel" onClick={ onHide }>取消</div>           <div className="button-confirm" onClick={ onHide }>确定</div>         </div >       </div >     </div >   ) } 

slot

import React, { useState, useEffect } from "react" import Dialog from './components/dialog'  export default function Slot() {   const [visible, setVisible] = useState(-1)    const onToggleVisible = () => {     setVisible(Math.random())   }    return (     <div className="slot">       <button onClick={ onToggleVisible }>切换dialog</button>       <Dialog         visible={visible}         title="默认插槽"       >         {/* 注意这里,会被Dialog组件的children读取并且替换掉 */}         <div className="slot-body">前端胖头鱼</div>       </Dialog>     </div>   ) }  

预览

默认插槽.gif

10. name slot(具名插槽)

当组件内部有多个动态内容需要外部来填充的时候,一个默认插槽已经不够用了,我们需要给插槽取个名字,这样外部才可以"按部就班"到指定位置。

我们来丰富一下Dialog组件,假设title也可以支持动态传递内容呢?

Vue

Vue中通过<slot name="main"></slot>形式先进行插槽的声明,再通过v-slot:main形式进行使用,一个萝卜一个坑也就填起来了

nameSlot源代码点这里

Dialog改造

<template>   <div class="dialog" v-show="visible">     <div class="dialog-mask" @click="onHide"></div>     <div class="dialog-body">       <div class="dialog-title" v-if="title">{{ title }}</div>       <!-- 注意这里,没有传title属性,时候通过插槽进行内容承接 -->       <slot name="title" v-else></slot>       <div class="dialog-main">         <!-- 声明main部分 -->         <slot name="main"></slot>       </div>       <div class="dialog-footer">         <div class="button-cancel" @click="onHide">取消</div>         <div class="button-confirm" @click="onHide">确定</div>       </div>     </div>   </div> </template> // ... 其他地方和上面试一样的 

nameSlot

<template>   <div class="slot">     <button @click="onToggleVisible">切换dialog</button>     <Dialog       :visible.sync="visible"     >       <template v-slot:title>         <div class="dialog-title">具名插槽</div>       </template>       <template v-slot:main>         <div class="slot-body">前端胖头鱼</div>       </template>     </Dialog>   </div> </template>  <script> import Dialog from './components/dialog.vue'  export default {   name: 'nameSlot',   components: {     Dialog,   },   data () {     return {       visible: false     }   },   methods: {     onToggleVisible () {       this.visible = !this.visible     }   } } </script>

React

前面通过props.children属性可以读取组件标签内的内容算是和Vue默认插槽实现了一样的功能,但是具名插槽如何实现呢?React好玩的其中一个点,我觉得是属性啥玩意都可以传、字符串数字函数连DOM也可以传。所以实现具名插槽也很简单,直接当属性传递就可以

nameSlot源代码点这里

Dialog改造

import React, { useState, useEffect } from "react"  import './dialog.css'  export default function Dialog(props) {   // 原谅我用visible -1这种傻叉的方式先实现了, 重点不是在这里   const { title, main, visible = -1 } = props   const [visibleInner, setVisibleInner] = useState(false)    const onHide = () => {     setVisibleInner(false)   }    useEffect(() => {     setVisibleInner(visible > 0)   }, [ visible ])    return (     <div className="dialog" style={ { display: visibleInner ? 'block' : 'none' }}>       <div className="dialog-mask" onClick={ onHide }></div>       <div className="dialog-body">         {/* { title ? <div className="dialog-title">{ title }</div> : null } */}         {/* 注意这里,直接渲染title就可以了 */}         { title }         <div className="dialog-main">           {/* 注意这里,通过children实现默认插槽功能 */}           {/* {children} */}           {/* 这一这里不是children了,是main */}           { main }         </div>         <div className="dialog-footer">           <div className="button-cancel" onClick={ onHide }>取消</div>           <div className="button-confirm" onClick={ onHide }>确定</div>         </div >       </div >     </div >   ) }  

nameSlot

import React, { useState } from "react" import Dialog from './components/dialog'  import './slot.css'  export default function NameSlot() {   const [visible, setVisible] = useState(-1)    const onToggleVisible = () => {     setVisible(Math.random())   }    return (     <div className="slot">       <button onClick={ onToggleVisible }>切换dialog</button>       <Dialog         visible={visible}         // 注意这里,直接传递的DOM         title={ <div className="dialog-title">默认插槽</div> }         // 注意这里,直接传递的DOM         main={ <div className="slot-body">前端胖头鱼</div> }       >       </Dialog>     </div>   ) } 

预览

可以看到具名插槽,React直接用属性反而更简洁一些

具名插槽.gif

11. scope slot(作用域插槽)

有了默认插槽具名插槽最后当然少不了作用域插槽啦!有时让插槽内容能够访问子组件中才有的数据是很有用的,这也是作用域插槽的意义所在

假设:Dialog组件内部有一个userInfo: { name: '前端胖头鱼' }数据对象,希望使用Dialog组件的外部插槽也能访问到,该怎么做呢?

Vue

scopeSlot源代码点这里

Dialog

<template>   <div class="dialog" v-show="visible">     <div class="dialog-mask" @click="onHide"></div>     <div class="dialog-body">       <div class="dialog-title" v-if="title">{{ title }}</div>       <!-- 注意这里,通过绑定userInfo外部可以进行使用 -->       <slot name="title" :userInfo="userInfo" v-else></slot>       <div class="dialog-main">         <!-- 注意这里,通过绑定userInfo外部可以进行使用 -->         <slot name="main" :userInfo="userInfo"></slot>       </div>       <div class="dialog-footer">         <div class="button-cancel" @click="onHide">取消</div>         <div class="button-confirm" @click="onHide">确定</div>       </div>     </div>   </div> </template>  <script> export default {   name: "dialog",   // ...   data () {     return {       userInfo: {         name: '前端胖头鱼'       }     }   },   // ...     }; </script> 

scopeSlot

<template>   <div class="slot">     <button @click="onToggleVisible">切换dialog</button>     <Dialog       :visible.sync="visible"     >       <template v-slot:title>         <div class="dialog-title">作用域插槽</div>       </template>       <!-- 注意这里 -->       <template v-slot:main="{ userInfo }">         <!-- 注意这里userInfo是Dialog组件内部的数据 -->         <div class="slot-body">你好{{ userInfo.name }}</div>       </template>     </Dialog>   </div> </template> 

React

还是那句话,React中万物皆可传,类似实现具名插槽中我们直接传递DOM,同样我们也可以传递函数,将Dialog组件内部的userInfo数据通过函数传参的方式给到外部使用

scopeSlot源代码点这里
Dialog改造

import React, { useState, useEffect } from "react"  import './dialog.css'  export default function Dialog(props) {   // 原谅我用visible -1这种傻叉的方式先实现了, 重点不是在这里   const { title, main, visible = -1 } = props   const [visibleInner, setVisibleInner] = useState(false)   const [ userInfo ] = useState({     name: '前端胖头鱼'   })    const onHide = () => {     setVisibleInner(false)   }    useEffect(() => {     setVisibleInner(visible > 0)   }, [ visible ])    return (     <div className="dialog" style={ { display: visibleInner ? 'block' : 'none' }}>       <div className="dialog-mask" onClick={ onHide }></div>       <div className="dialog-body">         {/* 作用域插槽,当函数使用,并且把数据传递进去 */}         { title(userInfo) }         <div className="dialog-main">           {/* 作用域插槽,当函数使用,并且把数据传递进去 */}           { main(userInfo) }         </div>         <div className="dialog-footer">           <div className="button-cancel" onClick={ onHide }>取消</div>           <div className="button-confirm" onClick={ onHide }>确定</div>         </div >       </div >     </div >   ) } 

scopeSlot

import React, { useState } from "react" import Dialog from './components/dialog'  import './slot.css'  export default function ScopeSlot() {   const [visible, setVisible] = useState(-1)    const onToggleVisible = () => {     setVisible(Math.random())   }    return (     <div className="slot">       <button onClick={ onToggleVisible }>切换dialog</button>       <Dialog         visible={visible}         // 通过函数来实现插槽         title={ () => <div className="dialog-title">作用域插槽</div> }         // 接收userInfo数据         main={ (userInfo) => <div className="slot-body">你好{ userInfo.name }</div> }       >       </Dialog>     </div>   ) }  

预览

作用域插槽.gif

TikTok 被指控“违反” OBS 相关 GPL 协议?或因新 Live Studio 直播应用程序而陷入争议

Posted: 20 Dec 2021 09:09 PM PST

据 THEVERGE 报道,上周晚些时候 TikTok 发布的新款 Live Studio Windows 应用程序代码因采用了开源流媒体项目(Open Broadcaster Software project) OBS Studio 的应用程序及其他开源项目,"未遵守各自的开源许可条款",而在推特上受到指控。

12 月 16 日,就有不少用户在推特上发布了疑似 Live Studio 代码的截图,也让该事件在推特上引发了不小的关注。

最初拍摄屏幕截图的程序员声称,该应用程序"是非法的 OBS 分支",并表示" TikTok 使用了OBS,然后在其顶部安装了自己的用户界面"。

还有一位推特用户指出,"如果 TikTok 确实使用了 OBS 的代码,则平台需要根据 GNU 通用公共许可证(GPL)第 2 版公开源代码。如果 TikTok 未能做到这一点,OBS 可能会对平台采取法律行动"。

12 月 17 日,OBS 的相关业务开发者 Ben Torell 在得到同意的情况下回复了这条推特。考虑到 OBS 对于和 TikTok 的合作持开放态度, Torell 在推文中表示:"我们承诺真诚地处理违反 GPL 的行为,对于 TikTok/ByteDance 来说,只要他们遵守许可证,我们很乐意与他们建立友好的工作关系。"

同时,Torell 证实了其团队在通过协议联系时发现了这些违规行为的"明确证据"。Torell 称已与 TikTok 方面取得联系,但尚未得到回应。

据悉,TikTok 上周低调发布的 Live Studio 应用程序,是一款基于 Windows 的应用程序,旨在帮助用户制作高质量的直播流,该程序支持直播整合视频游戏流、图像和文本叠加等功能。

据报道,几天前 TikTok 已经 "悄悄"与一小群用户一起测试了 Live Studio。目前状态下,Live Studio 似乎是一款"原装裸机"的流媒体软件,可支持用户通过相机、手机、游戏以及节目进行现场直播。据 TikTok 公司透漏,该应用程序目前仅面向几个市场的数千名用户可用。

OBS Studio 是一款受欢迎的应用程序,被许多直播流媒体使用。包括 Reddit 在内的许多公司也使用 OBS Studio 代码构建自己的直播软件。根据 OBS Studio 发布的 GPL 条款,这些公司也必须在同一许可证下公开任何修改的源代码。

上个月,OBS 刚刚卷入了一场与 Streamlabs 的"争端",这场争端也同样在 Twitter 上展开。事件起因是:OBS 承认并未允许 Streamlabs 以自己的名义使用 OBS,但 Streamlabs 还是使用了它。随后,由于 Pokimane 等流行的 streamers 公司都纷纷"威胁"停止使用和推广 Streamlabs,Streamlabs 最终才同意从其名称中删除了 "OBS"。

众所周知,如果项目或代码是开源的,如果操作得当,这类违规事件通常不会造成问题。同样 OBS 也是是开源的,但在这里, TikTok 或涉嫌未能遵守 OBS 的许可要求,因此才引起了不小关注。

当然,对于此次事件,Torell 也在推特上表示,OBS 软件正试图避免法律冲突。所以该事件的后续,我们也将持续关注,同时也期待两家公司后续能传来友好合作的相关消息。

2021 中国开源先锋 33 人评选启动,快来推荐你心尖上的开源先锋吧!

Posted: 21 Dec 2021 12:05 AM PST

image.png

前言

2021 年 3 月 13 日,《中华人民共和国国民经济和社会发展第十四个五年规划和 2035 年远景目标纲要》(以下简称"目标纲要",点击此处阅读全文)正式发布。"开源" 被首次写入国家 "五年规划",云计算、大数据、智能制造等关键词被多次提及。

"十四五" 是我国开启全面建设社会主义现代化国家新征程的第一个五年,全球新一轮科技革命和产业变革深入发展,软件和信息技术服务业迎来新的发展机遇。

SegmentFault 思否作为中国领先的新一代开发者社区,依托数百万开发者用户数据分析,及各科技企业厂商和个人在国内技术领域的行为、影响力指标,展开了 2021 年"中国技术先锋"年度评选。

在去年榜单的基础上,基于 SegmentFault 思否团队在全年对于 "开源" 领域的重点关注和观察,我们继续联合专业的开源联盟开源社共同推出 2021 年 "中国开源先锋 33 人之心尖上的开源人物" 年度评选。

自主申报将于即日起正式开始,诚邀各位开源同行者关注并于截止日期前推荐或自荐。

2021 中国开源先锋 33 人年度评选启动

2021 中国开源先锋 33 人

所谓先锋,我们理解不仅限于开发者,贡献代码的开发者、开源项目发起人、开源布道师、开源治理的先锋人物、关注开源的投资人等对推动开源生态发展有帮助的人都是榜单的目标对象。开源项目、开源治理、开源布道、开源商业、开源教育等都是我们重点关注的领域。

  • 不投票拉票,不要求转发;
  • 同时开放自主申报,或他人推荐;
  • 开源社理事王伟老师主持的《2021 中国开源年度报告》将作为本次评选的部分客观数据支撑;
  • 分不同维度推介,不设排名,都是 "心选";
  • 去年上榜者不重复入选,更加多元;
  • 欢迎去年上榜的先锋们作为 "师兄师姐" 为我们推荐今年的先锋人物,得到去年入选者的推荐,将被优先考虑。

这不是一个千篇一律的人物排行,而是 ——

  • 面向开发者的 "米其林推荐"(援引自开源社理事长庄表伟老师的提炼)
  • "心(舌)尖上的开源人物"(援引自开源社创始人刘天栋.Ted 和开源社理事李思颖的趣谈)

榜单发布时除发布名单,每位人物还都会配有推介理由,它既是对于 2021 年度的总结也是由 SegmentFault 思否和开源社为你联合推介的 "年度最值得关注的开源人物"。


这不是一份面向所有人的榜单,而是特别面向关注开源领域朋友们的信息参考。而本次评选之所以聚焦于 "人" 而非项目,是因为开源项目/开源商业都只是开源生态中的一环,而 "人" 才是万事万物的本源和基础。(这也是为什么 SegmentFault 2015 年起的第一个榜单就是基于人的 "TopWriter" 的评选。)

我们不依赖营销手段传播榜单,而是通过价值驱动。或许不一定榜单的每个领域都对你有所启发,但我们相信这份榜单中总有几位你不曾重点关注却能为你创造价值、带来启发的人,这便是这份榜单最大的价值。

我们期待有更多可能并不那么 "广为人知" 的开源人物也可以通过这份 "米其林推荐" 被更多人认识。除了开源项目、开源商业以外,开源教育、开源治理、开源知识产权等相对冷门的领域也有所发声。

评选流程 & 参选方式

  1. 报名征集:即日起至 2022 年 1 月 6 日
  2. 自荐/推荐 & 评选评审
  3. 榜单发布:2022 年 1 月中旬
  4. 申报链接:https://jinshuju.net/f/NPpbd4

自主申报即日起正式启动,欢迎点击 "报名链接",填写申报表,推荐或自荐。


评选咨询:pr@sifou.com

媒体合作:bd@sifou.com

相关阅读

MVCC 水略深,但是弄懂了真的好爽!

Posted: 20 Dec 2021 10:50 PM PST

@[toc]
前面写了一篇文章和大家分享了 MySQL 中查询表记录数的问题,里边涉及到一个知识点 MVCC 多版本并发控制。这个问题不搞懂,总感觉缺点什么。因此今天我想花点时间和大家聊一聊 MVCC。

要搞懂 MVCC,最好是要先懂 InnoDB 中事务的隔离级别,不然单纯看概念很难弄明白 MVCC。

1. 隔离级别

1.1 理论

MySQL 中事务的隔离级别一共分为四种,分别如下:

  • 序列化(SERIALIZABLE)
  • 可重复读(REPEATABLE READ)
  • 提交读(READ COMMITTED)
  • 未提交读(READ UNCOMMITTED)

四种不同的隔离级别含义分别如下:

  1. SERIALIZABLE
如果隔离级别为序列化,则用户之间通过一个接一个顺序地执行当前的事务,这种隔离级别提供了事务之间最大限度的隔离。
  1. REPEATABLE READ
在可重复读在这一隔离级别上,事务不会被看成是一个序列。不过,当前正在执行事务的变化仍然不能被外部看到,也就是说,如果用户在另外一个事务中执行同条 SELECT 语句数次,结果总是相同的。(因为正在执行的事务所产生的数据变化不能被外部看到)。
  1. READ COMMITTED
READ COMMITTED 隔离级别的安全性比 REPEATABLE READ 隔离级别的安全性要差。处于 READ COMMITTED 级别的事务可以看到其他事务对数据的修改。也就是说,在事务处理期间,如果其他事务修改了相应的表,那么同一个事务的多个 SELECT 语句可能返回不同的结果。
  1. READ UNCOMMITTED
READ UNCOMMITTED 提供了事务之间最小限度的隔离。除了容易产生虚幻的读操作和不能重复的读操作外,处于这个隔离级的事务可以读到其他事务还没有提交的数据,如果这个事务使用其他事务不提交的变化作为计算的基础,然后那些未提交的变化被它们的父事务撤销,这就导致了大量的数据变化。

在 MySQL 数据库种,默认的事务隔离级别是 REPEATABLE READ

1.2 SQL 实践

接下来通过几条简单的 SQL 向读者验证上面的理论。

1.2.1 查看隔离级别

通过如下 SQL 可以查看数据库实例默认的全局隔离级别和当前 session 的隔离级别:

MySQL8 之前使用如下命令查看 MySQL 隔离级别:

SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

查询结果如图:

可以看到,默认的隔离级别为 REPEATABLE-READ,全局隔离级别和当前会话隔离级别皆是如此。

MySQL8 开始,通过如下命令查看 MySQL 默认隔离级别

SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;

就是关键字变了,其他都一样。

通过如下命令可以修改隔离级别(建议开发者在修改时修改当前 session 隔离级别即可,不用修改全局的隔离级别):

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

上面这条 SQL 表示将当前 session 的数据库隔离级别设置为 READ UNCOMMITTED,设置成功后,再次查询隔离级别,发现当前 session 的隔离级别已经变了,如图1-2:

注意,如果只是修改了当前 session 的隔离级别,则换一个 session 之后,隔离级别又会恢复到默认的隔离级别,所以我们测试时,修改当前 session 的隔离级别即可。

1.2.2 READ UNCOMMITTED

1.2.2.1 准备测试数据

READ UNCOMMITTED 是最低隔离级别,这种隔离级别中存在脏读、不可重复读以及幻象读问题,所以这里我们先来看这个隔离级别,借此大家可以搞懂这三个问题到底是怎么回事。

下面分别予以介绍。

首先创建一个简单的表,预设两条数据,如下:

表的数据很简单,有 javaboy 和 itboyhub 两个用户,两个人的账户各有 1000 人民币。现在模拟这两个用户之间的一个转账操作。

注意,如果读者使用的是 Navicat 的话,不同的查询窗口就对应了不同的 session,如果读者使用了 SQLyog 的话,不同查询窗口对应同一个 session,因此如果使用 SQLyog,需要读者再开启一个新的连接,在新的连接中进行查询操作。

1.2.2.2 脏读

一个事务读到另外一个事务还没有提交的数据,称之为脏读。具体操作如下:

  1. 首先打开两个SQL操作窗口,假设分别为 A 和 B,在 A 窗口中输入如下几条 SQL (输入完成后不用执行):
START TRANSACTION; UPDATE account set balance=balance+100 where name='javaboy'; UPDATE account set balance=balance-100 where name='itboyhub'; COMMIT;
  1. 在 B 窗口执行如下 SQL,修改默认的事务隔离级别为 READ UNCOMMITTED,如下:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
  1. 接下来在 B 窗口中输入如下 SQL,输入完成后,首先执行第一行开启事务(注意只需要执行一行即可):
START TRANSACTION; SELECT * from account; COMMIT;
  1. 接下来执行 A 窗口中的前两条 SQL,即开启事务,给 javaboy 这个账户添加 100 元。
  2. 进入到 B 窗口,执行 B 窗口的第二条查询 SQL(SELECT * from user;),结果如下:

可以看到,A 窗口中的事务,虽然还未提交,但是 B 窗口中已经可以查询到数据的相关变化了。

这就是脏读问题。

1.2.2.3 不可重复读

不可重复读是指一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。具体操作步骤如下(操作之前先将两个账户的钱都恢复为1000):

  1. 首先打开两个查询窗口 A 和 B ,并且将 B 的数据库事务隔离级别设置为 READ UNCOMMITTED。具体 SQL 参考上文,这里不赘述。
  2. 在 B 窗口中输入如下 SQL,然后只执行前两条 SQL 开启事务并查询 javaboy 的账户:
START TRANSACTION; SELECT * from account where name='javaboy'; COMMIT;

前两条 SQL 执行结果如下:

  1. 在 A 窗口中执行如下 SQL,给 javaboy 这个账户添加 100 块钱,如下:
START TRANSACTION; UPDATE account set balance=balance+100 where name='javaboy'; COMMIT;

4.再次回到 B 窗口,执行 B 窗口的第二条 SQL 查看 javaboy 的账户,结果如下:

javaboy 的账户已经发生了变化,即前后两次查看 javaboy 账户,结果不一致,这就是不可重复读

和脏读的区别在于,脏读是看到了其他事务未提交的数据,而不可重复读是看到了其他事务已经提交的数据(由于当前 SQL 也是在事务中,因此有可能并不想看到其他事务已经提交的数据)。

1.2.2.4 幻象读

幻象读和不可重复读非常像,看名字就是产生幻觉了。

我举一个简单例子。

在 A 窗口中输入如下 SQL:

START TRANSACTION; insert into account(name,balance) values('zhangsan',1000); COMMIT;

然后在 B 窗口输入如下 SQL:

START TRANSACTION; SELECT * from account; delete from account where name='zhangsan'; COMMIT;

我们执行步骤如下:

  1. 首先执行 B 窗口的前两行,开启一个事务,同时查询数据库中的数据,此时查询到的数据只有 javaboy 和 itboyhub。
  2. 执行 A 窗口的前两行,向数据库中添加一个名为 zhangsan 的用户,注意不用提交事务。
  3. 执行 B 窗口的第二行,由于脏读问题,此时可以查询到 zhangsan 这个用户。
  4. 执行 B 窗口的第三行,去删除 name 为 zhangsan 的记录,这个时候删除就会出问题,虽然在 B 窗口中可以查询到 zhangsan,但是这条记录还没有提交,是因为脏读的原因才看到了,所以是没法删除的。此时就产生了幻觉,明明有个 zhangsan,却无法删除。

这就是幻读

看了上面的案例,大家应该明白了脏读不可重复读以及幻读各自是什么含义了。

1.2.3 READ COMMITTED

和 READ UNCOMMITTED 相比,READ COMMITTED 主要解决了脏读的问题,对于不可重复读和幻象读则未解决。

将事务的隔离级别改为 READ COMMITTED 之后,重复上面关于脏读案例的测试,发现已经不存在脏读问题了;重复上面关于不可重复读案例的测试,发现不可重复读问题依然存在。

上面那个案例不适用于幻读的测试,我们换一个幻读的测试案例。

还是两个窗口 A 和 B,将 B 窗口的隔离级别改为 READ COMMITTED

然后在 A 窗口输入如下测试 SQL:

START TRANSACTION; insert into account(name,balance) values('zhangsan',1000); COMMIT;

在 B 窗口输入如下测试 SQL:

START TRANSACTION; SELECT * from account; insert into account(name,balance) values('zhangsan',1000); COMMIT;

测试方式如下:

  1. 首先执行 B 窗口的前两行 SQL,开启事务并查询数据,此时查到的只有 javaboy 和 itboyhub 两个用户。
  2. 执行 A 窗口的前两行 SQL,插入一条记录,但是并不提交事务。
  3. 执行 B 窗口的第二行 SQL,由于现在已经没有了脏读问题,所以此时查不到 A 窗口中添加的数据。
  4. 执行 B 窗口的第三行 SQL,由于 name 字段唯一,因此这里会无法插入。此时就产生幻觉了,明明没有 zhangsan 这个用户,却无法插入 zhangsan。

1.2.4 REPEATABLE READ

和 READ COMMITTED 相比,REPEATABLE READ 进一步解决了不可重复读的问题,但是幻象读则未解决。

REPEATABLE READ 中关于幻读的测试和上一小节基本一致,不同的是第二步中执行完插入 SQL 后记得提交事务。

由于 REPEATABLE READ 已经解决了不可重复读,因此第二步即使提交了事务,第三步也查不到已经提交的数据,第四步继续插入就会出错。

注意,REPEATABLE READ 也是 InnoDB 引擎的默认数据库事务隔离级别

1.2.5 SERIALIZABLE

SERIALIZABLE 提供了事务之间最大限度的隔离,在这种隔离级别中,事务一个接一个顺序的执行,不会发生脏读、不可重复读以及幻象读问题,最安全。

如果设置当前事务隔离级别为 SERIALIZABLE,那么此时开启其他事务时,就会阻塞,必须等当前事务提交了,其他事务才能开启成功,因此前面的脏读、不可重复读以及幻象读问题这里都不会发生。

1.3 总结

总的来说,隔离级别和脏读、不可重复读以及幻象读的对应关系如下:

隔离级别脏读不可重复读幻象读
READ UNCOMMITTED允许允许允许
READ COMMITED不允许允许允许
REPEATABLE READ不允许不允许允许
SERIALIZABLE不允许不允许不允许

性能关系如图:

松哥前不久也录过一个隔离级别的视频,大家可以参考下:

2. 快照读与当前读

接下来我们还需要搞明白一个问题:快照读与当前读。

2.1 快照读

快照读(SnapShot Read)是一种一致性不加锁的读,是 InnoDB 存储引擎并发如此之高的核心原因之一。

在可重复读的隔离级别下,事务启动的时候,就会针对当前库拍一个照片(快照),快照读读取到的数据要么就是拍照时的数据,要么就是当前事务自身插入/修改过的数据。

我们日常所用的不加锁的查询,包括本文第一小节中涉及到的所有查询,都属于快照读,这个我就不演示了。

2.2 当前读

与快照读相对应的就是当前读,当前读就是读取最新数据,而不是历史版本的数据,换言之,在可重复读隔离级别下,如果使用了当前读,也可以读到别的事务已提交的数据。

松哥举个例子:

MySQL 事务开启两个会话 A 和 B。

首先在 A 会话中开启事务并查询 id 为 1 的记录:

接下来我们在 B 会话中对 id 为 1 的数据进行修改,如下:

注意 B 会话不要开启事务或者开启了及时提交事务,否则 update 语句占用一把排他锁会导致一会在 A 会话中用锁时发生阻塞。

接下来,回到 A 会话中继续做查询操作,如下:

可以看到,A 会话中第一个查询是快照读,读取到的是当前事务开启时的数据状态,后面两个查询则是当前读,读取到了当前最新的数据(B 会话中修改后的数据)。

3. undo log

我们再来稍微了解一下 undo log,这也有助于我们理解后面的 MVCC,这里我们简单介绍一下。

我们知道数据库事务有回滚的能力,既然能够回滚,那么就必须要在数据改变之前先把旧的数据记录下来,作为将来回滚的依据,那么这个记录就是 undo log。

当我们要添加一条记录的时候,就把添加的数据 id 记录到 undo log 中,将来回滚的时候就据此把数据删除;当我们要删除或者修改数据的时候,就把原数据记录到 undo log 中,将来据此恢复数据。查询操作因为不涉及回滚操作,所以就不需要记录到 undo log 中。

4. 行格式

接下来我们再来看一看行格式,这也有助于我们理解 MVCC。

行格式就是 InnoDB 在保存每一行的数据的时候,究竟是以什么样的格式来保存这行数据的。

数据库中的行格式有好几种,例如 COMPACT、REDUNDANT、DYNAMIC、COMPRESSED 等,不过无论是哪种行格式,都绕不开下面几个隐藏的数据列:

上图中的列 1、列 2、列 3 一直到列 N,就是我们数据库中表的列,保存着我们正常的数据,除了这些保存数据的列之外,还有三列额外加进来的数据,这也是我们这里要重点关注的 DB_ROW_IDDB_TRX_IDDB_ROLL_PTR 三列:

  • DB_ROW_ID:该列占用 6 个字节,是一个行 ID,用来唯一标识一行数据。如果用户在创建表的时候没有设置主键,那么系统会根据该列建立主键索引。
  • DB_TRX_ID:该列占用 6 个字节,是一个事务 ID。在 InnoDB 存储引擎中,当我们要开启一个事务的时候,会向 InnoDB 的事务系统申请一个事务 id,这个事务 id 是一个严格递增且唯一的数字,当前数据行是被哪个事务修改的,就会把对应的事务 id 记录在当前行中。
  • DB_ROLL_PTR:该列占用 7 个字节,是一个回滚指针,这个回滚指针指向一条 undo log 日志的地址,通过这个 undo log 日志可以让这条记录恢复到前一个版本。

好啦,这是关于数据行格式的一些内容。

5. MVCC

有了前面小节的预备知识,接下来我们就来正式看一看 MVCC。

MVCC,英文全称是 Multi-Version Concurrency Control,中文译作多版本并发控制。

MVCC 的核心思路就是保存数据行的历史版本,通过对数据行的多个版本进行管理来实现数据库的并发控制。

简单来说,我们平时看到的一条一条的记录,在数据库中保存的时候,可能不仅仅只有一条记录,而是有多个历史版本。

如下图:

这张图理解到位了,我想大家的 MVCC 也就理解的查不多了。

接下来我结合不同的隔离级别来和大家说这张图。

5.1 REPEATABLE READ

首先,当我们通过 INSERT\DELETE\UPDATE 去操作一行数据的时候,就会产生一个事务 id,这个事务 id 也会同时保存在行记录中(DB_TRX_ID),也就是说,当前数据行是哪个事务修改后得到的,是有记录的。

INSERT\DELETE\UPDATE 操作都会产生对应的 undo log 日志,每一行记录都有一个 DB_ROLL_PTR 指向 undo log 日志,每一行记录,通过执行 undo log 日志,就可以恢复到前一个记录、前前记录、前前前记录...

当我们开启一个事务的时候,首先会向 InnoDB 的事务系统申请一个事务 id,这个 id 是一个严格递增的数字,在当前事务开启的一瞬间系统会创建一个数组,数组中保存了目前所有的活跃事务 id,所谓的活跃事务就是指已开启但是还没有提交的事务。

这个数组中的最小值好理解,有的小伙伴可能会误以为数组中的最大值就是的当前事务的 id,其实这个不一定,也有可能更大。因为从申请到 trx_id 到创建数组之间也是需要时间的,这期间可能有其他会话也申请到了 trx_id。

当当前事务想要去查看某一行数据的时候,会先去查看该行数据的 DB_TRX_ID

  1. 如果这个值等于当前事务 id,说明这就是当前事务修改的,那么数据可见。
  2. 如果这个值小于数组中的最小值,说明当我们开启当前事务的时候,这行数据修改所涉及到的事务已经提交了,当前数据行是可见的。
  3. 如果这个值大于数组中的最大值,说明这行数据是我们在开启事务之后,还没有提交的时候,有另外一个会话也开启了事务,并且修改了这行数据,那么此时这行数据就是不可见的。
  4. 如果这个值的大小介于数组中最大值最小值之间(闭区间),且该值不在数组中,说明这也是一个已经提交的事务修改的数据,这是可见的。
  5. 如果这个值的大小介于数组中最大值最小值之间(闭区间),且该值在数组中(不等于当前事务 id),说明这是一个未提交的事务修改的数据,不可见。

前三种情况应该很好理解,主要是后面两种,松哥举一个简单例子。

比如我们有 A、B、C、D 四个会话,首先 A、B、C 分别开启一个事务,事务 ID 是 3、4、5,然后 C 会话提交了事务,A、B 未提交。接下来 D 会话也开启了一个事务,事务 ID 是 6,那么当 D 会话开启事务的时候,数组中的值就是 [3,4,6]。现在假设有一行数据的 DB_TRX_ID 是 5(第四种情况),那么该行数据就是可见的(因为当前事务开启的时候它已经提交了);如果有一行数据的 DB_TRX_ID 是 4,那么该行就不可见(因为未提交)。

另外还有一个需要注意的地方,就是如果当前事务中涉及到数据的更新操作,那么更新操作是在当前读的基础上更新的,而不是快照读的基础上更新的,如果是后者则有可能导致数据丢失。

我举一个例子,假设有如下表:

现在有两个会话 A 和 B,首先在 A 中开启事务:

然后在会话 B 中做一次修改操作(不用显式开启事务,更新 SQL 内部会开启事务,更新完成后事务会自动提交):

接下来回到会话 A 中,查询该条记录发现值没变,符合预期(目前隔离级别是可重复读),然后在 A 中做一次修改操作,修改完成后再去查询,如下图:

可以看到,更新其实是在 100 的基础上更新的,这个也好理解,要是在 99 的基础上更新,那么就会丢失掉 100 的那次更新,显然是不对的。

其实 MySQL 中的 update 就是先读再更新,读的时候默认就是当前读,即会加锁。所以在上面的案例中,如果 B 会话中显式的开启了事务并且没有没有提交,那么 A 会话中的 update 语句就会被阻塞。

这就是 MVCC,一行记录存在多个版本。实现了读写并发控制,读写互不阻塞;同时 MVCC 中采用了乐观锁,读数据不加锁,写数据只锁行,降低了死锁的概率;并且还能据此实现快照读。

5.2 READ COMMITTED

READ COMMITTED 和 REPEATABLE READ 类似,区别主要是后者在每次事务开始的时候创建一致性视图(创建数组列出活跃事务 id),而前者则每一个语句执行前都会重新算出一个新的视图。

所以 READ COMMITTED 这种隔离级别会看到别的会话已经提交的数据(即使别的会话比当前会话开启的晚)。

6. 小结

MVCC 在一定程度上实现了读写并发,不过它只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下有效。

而 READ UNCOMMITTED 总是会读取最新的数据行,SERIALIZABLE 则会对所有读取的行都加锁,这两个都和 MVCC 不兼容。

好啦,不知道小伙伴们看明白没有,有问题欢迎留言讨论。

倍受关注的 Cilium Service Mesh 到底怎么玩? - 上手实践

Posted: 20 Dec 2021 06:54 PM PST

大家好,我是张晋涛。

Cilium 是一个基于 eBPF 技术,用于为容器工作负载间提供安全且具备可观测性的网络连接的开源软件。

如果你对 Cilium 还不太了解,可以参考我之前的两篇文章:

最近 Cilium v1.11.0 正式发布了,增加 Open Telemetry 的支持以及其他一些增强特性。同时,也宣布了 Cilium Service Mesh 的计划。当前 Cilium Service Mesh 正处于测试阶段,预期在 2022 年会合并到 Cilium v1.12 版本中。

Cilium Service Mesh 也带来了一个全新的模式。

Cilium 直接通过 eBPF 技术实现的 Service Mesh 相比我们常规的 Istio/Linkerd 等方案,最显著的特点就是将 Sidecar proxy 模型替换成了 Kernel 模型, 如下图:

img

不再需要每个应用程序旁边都放置一个 Sidecar 了,直接在每台 Node 上提供支持。

img

我在几个月前就已经知道了这个消息并且进行了一些讨论,最近随着 isovalent 的一篇文章 How eBPF will solve Service Mesh - Goodbye Sidecars ,Cilium Service Mesh 也成为了大家关注的焦点。

本篇我带你实际体验下 Cilium Service Mesh。

安装部署

这里我使用 KIND 作为测试环境,我的内核版本是 5.15.8 。

准备 KIND 集群

关于 KIND 命令行工具的安装这里就不再赘述了,感兴趣的小伙伴可以参考我之前的文章 《使用KIND搭建自己的本地 Kubernetes 测试环境》。

以下是我创建集群使用的配置文件:

apiVersion: kind.x-k8s.io/v1alpha4 kind: Cluster nodes: - role: control-plane - role: worker - role: worker - role: worker networking:   disableDefaultCNI: true

创建集群:

➜  cilium-mesh kind create cluster --config kind-config.yaml  Creating cluster "kind" ...  ✓ Ensuring node image (kindest/node:v1.22.4) 🖼  ✓ Preparing nodes 📦 📦 📦 📦    ✓ Writing configuration 📜   ✓ Starting control-plane 🕹️   ✓ Installing StorageClass 💾   ✓ Joining worker nodes 🚜  Set kubectl context to "kind-kind" You can now use your cluster with:  kubectl cluster-info --context kind-kind  Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/

安装 Cilium CLI

这里我们使用 Cilium CLI 工具进行 Cilium 的部署。

➜  cilium-mesh curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz\{,.sha256sum\}  [1/2]: https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz --> cilium-linux-amd64.tar.gz --_curl_--https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                  Dload  Upload   Total   Spent    Left  Speed 100   154  100   154    0     0    243      0 --:--:-- --:--:-- --:--:--   242 100   664  100   664    0     0    579      0  0:00:01  0:00:01 --:--:--   579 100 14.6M  100 14.6M    0     0  2928k      0  0:00:05  0:00:05 --:--:-- 3910k  [2/2]: https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz.sha256sum --> cilium-linux-amd64.tar.gz.sha256sum --_curl_--https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz.sha256sum   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                  Dload  Upload   Total   Spent    Left  Speed 100   164  100   164    0     0    419      0 --:--:-- --:--:-- --:--:--   418 100   674  100   674    0     0    861      0 --:--:-- --:--:-- --:--:--   861 100    92  100    92    0     0     67      0  0:00:01  0:00:01 --:--:--     0 ➜  cilium-mesh ls cilium-linux-amd64.tar.gz  cilium-linux-amd64.tar.gz.sha256sum  kind-config.yaml ➜  cilium-mesh tar -zxvf cilium-linux-amd64.tar.gz  cilium

加载镜像

在部署 Cilium 的过程中需要一些镜像,我们可以提前下载后加载到 KIND 的 Node 节点中。如果你的网络比较顺畅, 那这一步可以跳过。

➜  cilium-mesh ciliumMeshImage=("quay.io/cilium/cilium-service-mesh:v1.11.0-beta.1" "quay.io/cilium/operator-generic-service-mesh:v1.11.0-beta.1" "quay.io/cilium/hubble-relay-service-mesh:v1.11.0-beta.1") ➜  cilium-mesh for i in ${ciliumMeshImage[@]} do   docker pull $i   kind load docker-image $i done

部署 cilium

接下来我们直接使用 Cilium CLI 完成部署。注意这里的参数。

➜  cilium-mesh cilium install --version -service-mesh:v1.11.0-beta.1 --config enable-envoy-config=true --kube-proxy-replacement=probe --agent-image='quay.io/cilium/cilium-service-mesh:v1.11.0-beta.1' --operator-image='quay.io/cilium/operator-generic-service-mesh:v1.11.0-beta.1'  --datapath-mode=vxlan  🔮 Auto-detected Kubernetes kind: kind ✨ Running "kind" validation checks ✅ Detected kind version "0.12.0" ℹ️  using Cilium version "-service-mesh:v1.11.0-beta.1" 🔮 Auto-detected cluster name: kind-kind 🔮 Auto-detected IPAM mode: kubernetes 🔮 Custom datapath mode: vxlan 🔑 Found CA in secret cilium-ca 🔑 Generating certificates for Hubble... 🚀 Creating Service accounts... 🚀 Creating Cluster roles... 🚀 Creating ConfigMap for Cilium version 1.11.0... ℹ️ Manual overwrite in ConfigMap: enable-envoy-config=true 🚀 Creating Agent DaemonSet... 🚀 Creating Operator Deployment... ⌛ Waiting for Cilium to be installed and ready... ✅ Cilium was successfully installed! Run 'cilium status' to view installation health

查看状态

在安装成功后, 可以通过 cilium status命令来查看当前 Cilium 的部署情况。

➜  cilium-mesh cilium status     /¯¯\  /¯¯\__/¯¯\    Cilium:         OK  \__/¯¯\__/    Operator:       OK  /¯¯\__/¯¯\    Hubble:         disabled  \__/¯¯\__/    ClusterMesh:    disabled     \__/  Deployment        cilium-operator    Desired: 1, Ready: 1/1, Available: 1/1 DaemonSet         cilium             Desired: 4, Ready: 4/4, Available: 4/4 Containers:       cilium             Running: 4                   cilium-operator    Running: 1 Cluster Pods:     3/3 managed by Cilium Image versions    cilium             quay.io/cilium/cilium-service-mesh:v1.11.0-beta.1: 4                   cilium-operator    quay.io/cilium/operator-generic-service-mesh:v1.11.0-beta.1: 1

启用 Hubble

Hubble 主要是用来提供可观测能力的。在启用它之前,需要先加载一个镜像,如果网络畅通可以跳过。

docker.io/envoyproxy/envoy:v1.18.2@sha256:e8b37c1d75787dd1e712ff389b0d37337dc8a174a63bed9c34ba73359dc67da7

然后使用 Cilium CLI 开启 Hubble :

➜  cilium-mesh cilium hubble enable --relay-image='quay.io/cilium/hubble-relay-service-mesh:v1.11.0-beta.1' --ui 🔑 Found CA in secret cilium-ca                                                                       ✨ Patching ConfigMap cilium-config to enable Hubble...                     ♻️  Restarted Cilium pods                                                                              ⌛ Waiting for Cilium to become ready before deploying other Hubble component(s)... 🔑 Generating certificates for Relay...         ✨ Deploying Relay from quay.io/cilium/hubble-relay-service-mesh:v1.11.0-beta.1... ✨ Deploying Hubble UI from quay.io/cilium/hubble-ui:v0.8.3 and Hubble UI Backend from quay.io/cilium/hubble-ui-backend:v0.8.3... ⌛ Waiting for Hubble to be installed...            /¯¯\                                 /¯¯\__/¯¯\    Cilium:         OK                                                                      \__/¯¯\__/    Operator:       OK                                                                                                                                                                           /¯¯\__/¯¯\    Hubble:         OK                                                                      \__/¯¯\__/    ClusterMesh:    disabled                                                                   \__/                                                                                                                                                                                                    DaemonSet         cilium             Desired: 4, Ready: 4/4, Available: 4/4 Deployment        cilium-operator    Desired: 1, Ready: 1/1, Available: 1/1 Deployment        hubble-relay       Desired: 1, Ready: 1/1, Available: 1/1 Deployment        hubble-ui          Desired: 1, Unavailable: 1/1 Containers:       cilium             Running: 4                   cilium-operator    Running: 1                   hubble-relay       Running: 1                   hubble-ui          Running: 1 Cluster Pods:     5/5 managed by Cilium Image versions    cilium             quay.io/cilium/cilium-service-mesh:v1.11.0-beta.1: 4                   cilium-operator    quay.io/cilium/operator-generic-service-mesh:v1.11.0-beta.1: 1                   hubble-relay       quay.io/cilium/hubble-relay-service-mesh:v1.11.0-beta.1: 1                   hubble-ui          quay.io/cilium/hubble-ui:v0.8.3: 1                   hubble-ui          quay.io/cilium/hubble-ui-backend:v0.8.3: 1                   hubble-ui          docker.io/envoyproxy/envoy:v1.18.2@sha256:e8b37c1d75787dd1e712ff389b0d37337dc8a174a63bed9c34ba73359dc67da7: 1

测试 7 层 Ingress 流量管理

安装LB

这里我们可以给 KIND 集群中安装 MetaLB ,以便于我们可以使用 LoadBalancer 类型的 svc 资源(Cilium 会默认创建一个 LoadBalancer 类型的 svc)。如果不安装 MetaLB ,那也可以使用 NodePort 的方式来进行替代。

具体过程就不一一介绍了,直接按下述操作步骤执行即可。

➜  cilium-mesh kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/master/manifests/namespace.yaml  namespace/metallb-system created ➜  cilium-mesh kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"   secret/memberlist created ➜  cilium-mesh kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/master/manifests/metallb.yaml Warning: policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+ podsecuritypolicy.policy/controller created podsecuritypolicy.policy/speaker created serviceaccount/controller created serviceaccount/speaker created clusterrole.rbac.authorization.k8s.io/metallb-system:controller created clusterrole.rbac.authorization.k8s.io/metallb-system:speaker created role.rbac.authorization.k8s.io/config-watcher created role.rbac.authorization.k8s.io/pod-lister created role.rbac.authorization.k8s.io/controller created clusterrolebinding.rbac.authorization.k8s.io/metallb-system:controller created clusterrolebinding.rbac.authorization.k8s.io/metallb-system:speaker created rolebinding.rbac.authorization.k8s.io/config-watcher created rolebinding.rbac.authorization.k8s.io/pod-lister created rolebinding.rbac.authorization.k8s.io/controller created daemonset.apps/speaker created deployment.apps/controller created ➜  cilium-mesh docker network inspect -f '{{.IPAM.Config}}' kind [{172.18.0.0/16  172.18.0.1 map[]} {fc00:f853:ccd:e793::/64  fc00:f853:ccd:e793::1 map[]}] ➜  cilium-mesh vim kind-lb-cm.yaml ➜  cilium-mesh cat kind-lb-cm.yaml  apiVersion: v1 kind: ConfigMap metadata:   namespace: metallb-system   name: config data:   config: |     address-pools:     - name: default       protocol: layer2       addresses:       - 172.18.255.200-172.18.255.250 ➜  cilium-mesh kubectl apply  -f kind-lb-cm.yaml configmap/config created

加载镜像

这里我们使用 hashicorp/http-echo:0.2.3作为示例程序,它们可以按照启动参数的不同响应不同的内容。

➜  cilium-mesh docker pull hashicorp/http-echo:0.2.3 0.2.3: Pulling from hashicorp/http-echo 86399148984b: Pull complete  Digest: sha256:ba27d460cd1f22a1a4331bdf74f4fccbc025552357e8a3249c40ae216275de96 Status: Downloaded newer image for hashicorp/http-echo:0.2.3 docker.io/hashicorp/http-echo:0.2.3 ➜  cilium-mesh kind load docker-image hashicorp/http-echo:0.2.3  Image: "hashicorp/http-echo:0.2.3" with ID "sha256:a6838e9a6ff6ab3624720a7bd36152dda540ce3987714398003e14780e61478a" not yet present on node "kind-worker", loading... Image: "hashicorp/http-echo:0.2.3" with ID "sha256:a6838e9a6ff6ab3624720a7bd36152dda540ce3987714398003e14780e61478a" not yet present on node "kind-worker2", loading... Image: "hashicorp/http-echo:0.2.3" with ID "sha256:a6838e9a6ff6ab3624720a7bd36152dda540ce3987714398003e14780e61478a" not yet present on node "kind-control-plane", loading... Image: "hashicorp/http-echo:0.2.3" with ID "sha256:a6838e9a6ff6ab3624720a7bd36152dda540ce3987714398003e14780e61478a" not yet present on node "kind-worker3", loading...

部署测试服务

本文中的所有配置文件均可在 https://github.com/tao1234566... 代码仓库中获取。

我们使用如下配置进行测试服务的部署:

apiVersion: v1 kind: Pod metadata:   labels:     run: foo-app   name: foo-app spec:   containers:   - image: hashicorp/http-echo:0.2.3     args:     - "-text=foo"     name: foo-app     ports:     - containerPort: 5678     resources: {}   dnsPolicy: ClusterFirst   restartPolicy: Always status: {} --- apiVersion: v1 kind: Service metadata:   labels:     run: foo-app   name: foo-app spec:   ports:   - port: 5678     protocol: TCP     targetPort: 5678   selector:     run: foo-app --- apiVersion: v1 kind: Pod metadata:   labels:     run: bar-app   name: bar-app spec:   containers:   - image: hashicorp/http-echo:0.2.3     args:     - "-text=bar"     name: bar-app     ports:     - containerPort: 5678     resources: {}   dnsPolicy: ClusterFirst   restartPolicy: Always --- apiVersion: v1 kind: Service metadata:   labels:     run: bar-app   name: bar-app spec:   ports:   - port: 5678     protocol: TCP     targetPort: 5678   selector:     run: bar-app 

新建如下的 Ingress 资源文件:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata:   name: cilium-ingress   namespace: default spec:   ingressClassName: cilium   rules:   - http:       paths:       - backend:           service:             name: foo-app             port:               number: 5678         path: /foo         pathType: Prefix       - backend:           service:             name: bar-app             port:               number: 5678         path: /bar         pathType: Prefix

创建 Ingress 资源,然后可以看到产生了一个新的 LoadBalancer 类型的 svc 。

➜  cilium-mesh kubectl apply -f cilium-ingress.yaml ingress.networking.k8s.io/cilium-ingress created ➜  cilium-mesh kubectl get svc NAME                            TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE bar-app                         ClusterIP      10.96.229.141   <none>           5678/TCP       106s cilium-ingress-cilium-ingress   LoadBalancer   10.96.161.128   172.18.255.200   80:31643/TCP   4s foo-app                         ClusterIP      10.96.166.212   <none>           5678/TCP       106s kubernetes                      ClusterIP      10.96.0.1       <none>           443/TCP        81m  ➜  cilium-mesh kubectl get ing NAME             CLASS    HOSTS   ADDRESS          PORTS   AGE cilium-ingress   cilium   *       172.18.255.200   80      1m

测试

使用 curl 命令进行测试访问,发现可以按照 Ingress 资源中的配置得到正确的响应。查看响应头,我们会发现这里的代理实际上还是使用的 Envoy 来完成的。

➜  cilium-mesh curl 172.18.255.200 ➜  cilium-mesh curl 172.18.255.200/foo foo ➜  cilium-mesh curl 172.18.255.200/bar bar ➜  cilium-mesh curl -I 172.18.255.200/bar HTTP/1.1 200 OK Content-Length: 4 Connection: keep-alive Content-Type: text/plain; charset=utf-8 Date: Sat, 18 Dec 2021 06:02:56 GMT Keep-Alive: timeout=4 Proxy-Connection: keep-alive Server: envoy X-App-Name: http-echo X-App-Version: 0.2.3 X-Envoy-Upstream-Service-Time: 0  ➜  cilium-mesh curl -I 172.18.255.200/foo HTTP/1.1 200 OK Content-Length: 4 Connection: keep-alive Content-Type: text/plain; charset=utf-8 Date: Sat, 18 Dec 2021 06:03:01 GMT Keep-Alive: timeout=4 Proxy-Connection: keep-alive Server: envoy X-App-Name: http-echo X-App-Version: 0.2.3 X-Envoy-Upstream-Service-Time: 0  

测试 CiliumEnvoyConfig

在使用上述方式部署 CIlium 后, 它其实还安装了一些 CRD 资源。其中有一个是 CiliumEnvoyConfig用于配置服务之间代理的。

➜  cilium-mesh kubectl api-resources |grep cilium.io ciliumclusterwidenetworkpolicies   ccnp           cilium.io/v2                           false        CiliumClusterwideNetworkPolicy ciliumendpoints                    cep,ciliumep   cilium.io/v2                           true         CiliumEndpoint ciliumenvoyconfigs                 cec            cilium.io/v2alpha1                     false        CiliumEnvoyConfig ciliumexternalworkloads            cew            cilium.io/v2                           false        CiliumExternalWorkload ciliumidentities                   ciliumid       cilium.io/v2                           false        CiliumIdentity ciliumnetworkpolicies              cnp,ciliumnp   cilium.io/v2                           true         CiliumNetworkPolicy ciliumnodes                        cn,ciliumn     cilium.io/v2                           false        CiliumNode

部署测试服务

可以先进行 Hubble 的 port-forward

➜  cilium-mesh cilium hubble port-forward

默认会监听到 4245 端口上,如果不提前执行此操作就会出现下述内容

🔭 Enabling Hubble telescope... ⚠️  Unable to contact Hubble Relay, disabling Hubble telescope and flow validation: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp [::1]:4245: connect: connection refused"

如果已经开启 Hubble 的 port-forward ,正常情况下会得到如下输出:

➜  cilium-mesh cilium connectivity test --test egress-l7 ℹ️  Monitor aggregation detected, will skip some flow validation steps ⌛ [kind-kind] Waiting for deployments [client client2 echo-same-node] to become ready... ⌛ [kind-kind] Waiting for deployments [echo-other-node] to become ready... ⌛ [kind-kind] Waiting for CiliumEndpoint for pod cilium-test/client-6488dcf5d4-pk6w9 to appear... ⌛ [kind-kind] Waiting for CiliumEndpoint for pod cilium-test/client2-5998d566b4-hrhrb to appear... ⌛ [kind-kind] Waiting for CiliumEndpoint for pod cilium-test/echo-other-node-f4d46f75b-bqpcb to appear... ⌛ [kind-kind] Waiting for CiliumEndpoint for pod cilium-test/echo-same-node-745bd5c77-zpzdn to appear... ⌛ [kind-kind] Waiting for Service cilium-test/echo-other-node to become ready... ⌛ [kind-kind] Waiting for Service cilium-test/echo-same-node to become ready... ⌛ [kind-kind] Waiting for NodePort 172.18.0.5:32751 (cilium-test/echo-other-node) to become ready... ⌛ [kind-kind] Waiting for NodePort 172.18.0.5:32133 (cilium-test/echo-same-node) to become ready... ⌛ [kind-kind] Waiting for NodePort 172.18.0.3:32133 (cilium-test/echo-same-node) to become ready... ⌛ [kind-kind] Waiting for NodePort 172.18.0.3:32751 (cilium-test/echo-other-node) to become ready... ⌛ [kind-kind] Waiting for NodePort 172.18.0.2:32751 (cilium-test/echo-other-node) to become ready... ⌛ [kind-kind] Waiting for NodePort 172.18.0.2:32133 (cilium-test/echo-same-node) to become ready... ⌛ [kind-kind] Waiting for NodePort 172.18.0.4:32751 (cilium-test/echo-other-node) to become ready... ⌛ [kind-kind] Waiting for NodePort 172.18.0.4:32133 (cilium-test/echo-same-node) to become ready... ℹ️  Skipping IPCache check ⌛ [kind-kind] Waiting for pod cilium-test/client-6488dcf5d4-pk6w9 to reach default/kubernetes service... ⌛ [kind-kind] Waiting for pod cilium-test/client2-5998d566b4-hrhrb to reach default/kubernetes service... 🔭 Enabling Hubble telescope... ℹ️  Hubble is OK, flows: 16380/16380 🏃 Running tests...  [=] Skipping Test [no-policies]  [=] Skipping Test [allow-all]  [=] Skipping Test [client-ingress]  [=] Skipping Test [echo-ingress]  [=] Skipping Test [client-egress]  [=] Skipping Test [to-entities-world]  [=] Skipping Test [to-cidr-1111]  [=] Skipping Test [echo-ingress-l7]  [=] Test [client-egress-l7] .......... [=] Skipping Test [dns-only]  [=] Skipping Test [to-fqdns]  ✅ All 1 tests (10 actions) successful, 10 tests skipped, 0 scenarios skipped.

我们也可以同时打开UI看看:

➜  cilium-mesh cilium hubble ui    ℹ️  Opening "http://localhost:12000" in your browser...

效果图如下:

img

这个操作实际上会进行如下部署:

➜  cilium-mesh kubectl -n cilium-test get all NAME                                  READY   STATUS    RESTARTS   AGE pod/client-6488dcf5d4-pk6w9           1/1     Running   0          66m pod/client2-5998d566b4-hrhrb          1/1     Running   0          66m pod/echo-other-node-f4d46f75b-bqpcb   1/1     Running   0          66m pod/echo-same-node-745bd5c77-zpzdn    1/1     Running   0          66m  NAME                      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE service/echo-other-node   NodePort   10.96.124.211   <none>        8080:32751/TCP   66m service/echo-same-node    NodePort   10.96.136.252   <none>        8080:32133/TCP   66m  NAME                              READY   UP-TO-DATE   AVAILABLE   AGE deployment.apps/client            1/1     1            1           66m deployment.apps/client2           1/1     1            1           66m deployment.apps/echo-other-node   1/1     1            1           66m deployment.apps/echo-same-node    1/1     1            1           66m  NAME                                        DESIRED   CURRENT   READY   AGE replicaset.apps/client-6488dcf5d4           1         1         1       66m replicaset.apps/client2-5998d566b4          1         1         1       66m replicaset.apps/echo-other-node-f4d46f75b   1         1         1       66m replicaset.apps/echo-same-node-745bd5c77    1         1         1       66m

我们也可以看看它的 label:

➜  cilium-mesh kubectl get pods -n cilium-test --show-labels -o wide  NAME                              READY   STATUS    RESTARTS   AGE   IP             NODE           NOMINATED NODE   READINESS GATES   LABELS client-6488dcf5d4-pk6w9           1/1     Running   0          67m   10.244.3.7     kind-worker3   <none>           <none>            kind=client,name=client,pod-template-hash=6488dcf5d4 client2-5998d566b4-hrhrb          1/1     Running   0          67m   10.244.3.18    kind-worker3   <none>           <none>            kind=client,name=client2,other=client,pod-template-hash=5998d566b4 echo-other-node-f4d46f75b-bqpcb   1/1     Running   0          67m   10.244.1.146   kind-worker2   <none>           <none>            kind=echo,name=echo-other-node,pod-template-hash=f4d46f75b echo-same-node-745bd5c77-zpzdn    1/1     Running   0          67m   10.244.3.164   kind-worker3   <none>           <none>            kind=echo,name=echo-same-node,other=echo,pod-template-hash=745bd5c77

测试

这里我们在主机上进行操作下, 先拿到 client2 的 Pod 名称,然后通过 Hubble 命令观察所有访问此 Pod 的流量。

➜  cilium-mesh export CLIENT2=client2-5998d566b4-hrhrb ➜  cilium-mesh hubble observe --from-pod cilium-test/$CLIENT2 -f  Dec 18 14:07:37.200: cilium-test/client2-5998d566b4-hrhrb:44805 <> kube-system/coredns-78fcd69978-7lbwh:53 to-overlay FORWARDED (UDP) Dec 18 14:07:37.200: cilium-test/client2-5998d566b4-hrhrb:44805 -> kube-system/coredns-78fcd69978-7lbwh:53 to-endpoint FORWARDED (UDP) Dec 18 14:07:37.200: cilium-test/client2-5998d566b4-hrhrb:44805 <> kube-system/coredns-78fcd69978-7lbwh:53 to-overlay FORWARDED (UDP) Dec 18 14:07:37.200: cilium-test/client2-5998d566b4-hrhrb:44805 -> kube-system/coredns-78fcd69978-7lbwh:53 to-endpoint FORWARDED (UDP) Dec 18 14:07:37.200: cilium-test/client2-5998d566b4-hrhrb:42260 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-endpoint FORWARDED (TCP Flags: SYN) Dec 18 14:07:37.201: cilium-test/client2-5998d566b4-hrhrb:42260 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-endpoint FORWARDED (TCP Flags: ACK) Dec 18 14:07:37.201: cilium-test/client2-5998d566b4-hrhrb:42260 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-endpoint FORWARDED (TCP Flags: ACK, PSH) Dec 18 14:07:37.202: cilium-test/client2-5998d566b4-hrhrb:42260 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-endpoint FORWARDED (TCP Flags: ACK, FIN) Dec 18 14:07:37.203: cilium-test/client2-5998d566b4-hrhrb:42260 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-endpoint FORWARDED (TCP Flags: ACK) Dec 18 14:07:50.769: cilium-test/client2-5998d566b4-hrhrb:36768 <> kube-system/coredns-78fcd69978-7lbwh:53 to-overlay FORWARDED (UDP) Dec 18 14:07:50.769: cilium-test/client2-5998d566b4-hrhrb:36768 <> kube-system/coredns-78fcd69978-7lbwh:53 to-overlay FORWARDED (UDP) Dec 18 14:07:50.769: cilium-test/client2-5998d566b4-hrhrb:36768 -> kube-system/coredns-78fcd69978-7lbwh:53 to-endpoint FORWARDED (UDP) Dec 18 14:07:50.769: cilium-test/client2-5998d566b4-hrhrb:36768 -> kube-system/coredns-78fcd69978-7lbwh:53 to-endpoint FORWARDED (UDP) Dec 18 14:07:50.770: cilium-test/client2-5998d566b4-hrhrb:42068 <> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-overlay FORWARDED (TCP Flags: SYN) Dec 18 14:07:50.770: cilium-test/client2-5998d566b4-hrhrb:42068 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-endpoint FORWARDED (TCP Flags: SYN) Dec 18 14:07:50.770: cilium-test/client2-5998d566b4-hrhrb:42068 <> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-overlay FORWARDED (TCP Flags: ACK) Dec 18 14:07:50.770: cilium-test/client2-5998d566b4-hrhrb:42068 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-endpoint FORWARDED (TCP Flags: ACK) Dec 18 14:07:50.770: cilium-test/client2-5998d566b4-hrhrb:42068 <> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-overlay FORWARDED (TCP Flags: ACK, PSH) Dec 18 14:07:50.770: cilium-test/client2-5998d566b4-hrhrb:42068 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-endpoint FORWARDED (TCP Flags: ACK, PSH) Dec 18 14:07:50.771: cilium-test/client2-5998d566b4-hrhrb:42068 <> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-overlay FORWARDED (TCP Flags: ACK, FIN) Dec 18 14:07:50.771: cilium-test/client2-5998d566b4-hrhrb:42068 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-endpoint FORWARDED (TCP Flags: ACK, FIN) Dec 18 14:07:50.772: cilium-test/client2-5998d566b4-hrhrb:42068 <> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-overlay FORWARDED (TCP Flags: ACK) Dec 18 14:07:50.772: cilium-test/client2-5998d566b4-hrhrb:42068 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-endpoint FORWARDED (TCP Flags: ACK) 

以上输出是由于我们执行了下面的操作:

kubectl exec -it -n cilium-test $CLIENT2 -- curl -v echo-same-node:8080/ kubectl exec -it -n cilium-test $CLIENT2 -- curl -v echo-other-node:8080/

日志中基本上都是 to-endpoint 或者 to-overlay的。

测试使用 proxy

需要先安装 networkpolicy , 我们可以直接从 Cilium CLI 的仓库中拿到。

kubectl apply -f https://raw.githubusercontent.com/cilium/cilium-cli/master/connectivity/manifests/client-egress-l7-http.yaml kubectl apply -f https://raw.githubusercontent.com/cilium/cilium-cli/master/connectivity/manifests/client-egress-only-dns.yaml

然后重复上面的请求:

Dec 18 14:33:40.570: cilium-test/client2-5998d566b4-hrhrb:44344 -> kube-system/coredns-78fcd69978-2ww28:53 L3-L4 REDIRECTED (UDP) Dec 18 14:33:40.570: cilium-test/client2-5998d566b4-hrhrb:44344 -> kube-system/coredns-78fcd69978-2ww28:53 to-proxy FORWARDED (UDP) Dec 18 14:33:40.570: cilium-test/client2-5998d566b4-hrhrb:44344 -> kube-system/coredns-78fcd69978-2ww28:53 to-proxy FORWARDED (UDP) Dec 18 14:33:40.570: cilium-test/client2-5998d566b4-hrhrb:44344 -> kube-system/coredns-78fcd69978-2ww28:53 dns-request FORWARDED (DNS Query echo-other-node.cilium-test.svc.cluster.local. A) Dec 18 14:33:40.570: cilium-test/client2-5998d566b4-hrhrb:44344 -> kube-system/coredns-78fcd69978-2ww28:53 dns-request FORWARDED (DNS Query echo-other-node.cilium-test.svc.cluster.local. AAAA) Dec 18 14:33:40.571: cilium-test/client2-5998d566b4-hrhrb:42074 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 L3-L4 REDIRECTED (TCP Flags: SYN) Dec 18 14:33:40.571: cilium-test/client2-5998d566b4-hrhrb:42074 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-proxy FORWARDED (TCP Flags: SYN) Dec 18 14:33:40.571: cilium-test/client2-5998d566b4-hrhrb:42074 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-proxy FORWARDED (TCP Flags: ACK) Dec 18 14:33:40.571: cilium-test/client2-5998d566b4-hrhrb:42074 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 14:33:40.572: cilium-test/client2-5998d566b4-hrhrb:42074 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 http-request FORWARDED (HTTP/1.1 GET http://echo-other-node:8080/) Dec 18 14:33:40.573: cilium-test/client2-5998d566b4-hrhrb:42074 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-proxy FORWARDED (TCP Flags: ACK, FIN) Dec 18 14:33:40.573: cilium-test/client2-5998d566b4-hrhrb:42074 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-proxy FORWARDED (TCP Flags: ACK)

执行另一个请求:

➜  cilium-mesh kubectl exec -it -n cilium-test $CLIENT2 -- curl -v echo-same-node:8080/

也可以看到如下输出,其中有 to-proxy的字样。

Dec 18 14:45:18.857: cilium-test/client2-5998d566b4-hrhrb:58895 -> kube-system/coredns-78fcd69978-2ww28:53 L3-L4 REDIRECTED (UDP) Dec 18 14:45:18.857: cilium-test/client2-5998d566b4-hrhrb:58895 -> kube-system/coredns-78fcd69978-2ww28:53 to-proxy FORWARDED (UDP) Dec 18 14:45:18.857: cilium-test/client2-5998d566b4-hrhrb:58895 -> kube-system/coredns-78fcd69978-2ww28:53 to-proxy FORWARDED (UDP) Dec 18 14:45:18.857: cilium-test/client2-5998d566b4-hrhrb:58895 -> kube-system/coredns-78fcd69978-2ww28:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. AAAA) Dec 18 14:45:18.857: cilium-test/client2-5998d566b4-hrhrb:58895 -> kube-system/coredns-78fcd69978-2ww28:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. A) Dec 18 14:45:18.858: cilium-test/client2-5998d566b4-hrhrb:42266 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 L3-L4 REDIRECTED (TCP Flags: SYN) Dec 18 14:45:18.858: cilium-test/client2-5998d566b4-hrhrb:42266 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: SYN) Dec 18 14:45:18.858: cilium-test/client2-5998d566b4-hrhrb:42266 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK) Dec 18 14:45:18.858: cilium-test/client2-5998d566b4-hrhrb:42266 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 14:45:18.858: cilium-test/client2-5998d566b4-hrhrb:42266 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 http-request FORWARDED (HTTP/1.1 GET http://echo-same-node:8080/) Dec 18 14:45:18.859: cilium-test/client2-5998d566b4-hrhrb:42266 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK, FIN) Dec 18 14:45:18.859: cilium-test/client2-5998d566b4-hrhrb:42266 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK)

其实看请求头更加方便:

➜  cilium-mesh kubectl exec -it -n cilium-test $CLIENT2 -- curl -I echo-same-node:8080/ HTTP/1.1 403 Forbidden content-length: 15 content-type: text/plain date: Sat, 18 Dec 2021 14:47:39 GMT server: envoy

之前都是如下:

## 没有 proxy ➜  cilium-mesh kubectl exec -it -n cilium-test $CLIENT2 -- curl -v echo-same-node:8080/               *   Trying 10.96.136.252:8080...                                                                      * Connected to echo-same-node (10.96.136.252) port 8080 (#0)                                          > GET / HTTP/1.1                                                                                      > Host: echo-same-node:8080                                                                           > User-Agent: curl/7.78.0                          > Accept: */*                                                                                         >                                                                                                     * Mark bundle as not supporting multiuse                                                              < HTTP/1.1 200 OK                                                                                     < X-Powered-By: Express                                                                               < Vary: Origin, Accept-Encoding                                                                       < Access-Control-Allow-Credentials: true                                                              < Accept-Ranges: bytes                                                                                < Cache-Control: public, max-age=0                                                                    < Last-Modified: Sat, 26 Oct 1985 08:15:00 GMT                                                        < ETag: W/"809-7438674ba0"                                                                            < Content-Type: text/html; charset=UTF-8                                                              < Content-Length: 2057                                                                                < Date: Sat, 18 Dec 2021 14:07:37 GMT                                                                 < Connection: keep-alive                                                                              < Keep-Alive: timeout=5   

请求一个不存在的地址:

以前请求响应是 404 ,现在是 403 ,并得到如下内容

➜  cilium-mesh kubectl exec -it -n cilium-test $CLIENT2 -- curl -v echo-same-node:8080/foo *   Trying 10.96.136.252:8080... * Connected to echo-same-node (10.96.136.252) port 8080 (#0) > GET /foo HTTP/1.1 > Host: echo-same-node:8080 > User-Agent: curl/7.78.0 > Accept: */* >  * Mark bundle as not supporting multiuse < HTTP/1.1 403 Forbidden < content-length: 15 < content-type: text/plain < date: Sat, 18 Dec 2021 14:50:38 GMT < server: envoy <  Access denied * Connection #0 to host echo-same-node left intact

日志中也都是 to-proxy的字样。

Dec 18 14:50:39.185: cilium-test/client2-5998d566b4-hrhrb:37683 -> kube-system/coredns-78fcd69978-7lbwh:53 L3-L4 REDIRECTED (UDP) Dec 18 14:50:39.185: cilium-test/client2-5998d566b4-hrhrb:37683 -> kube-system/coredns-78fcd69978-7lbwh:53 to-proxy FORWARDED (UDP) Dec 18 14:50:39.185: cilium-test/client2-5998d566b4-hrhrb:37683 -> kube-system/coredns-78fcd69978-7lbwh:53 to-proxy FORWARDED (UDP) Dec 18 14:50:39.185: cilium-test/client2-5998d566b4-hrhrb:37683 -> kube-system/coredns-78fcd69978-7lbwh:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. AAAA) Dec 18 14:50:39.185: cilium-test/client2-5998d566b4-hrhrb:37683 -> kube-system/coredns-78fcd69978-7lbwh:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. A) Dec 18 14:50:39.186: cilium-test/client2-5998d566b4-hrhrb:42274 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 L3-L4 REDIRECTED (TCP Flags: SYN) Dec 18 14:50:39.186: cilium-test/client2-5998d566b4-hrhrb:42274 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: SYN) Dec 18 14:50:39.186: cilium-test/client2-5998d566b4-hrhrb:42274 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK) Dec 18 14:50:39.186: cilium-test/client2-5998d566b4-hrhrb:42274 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 14:50:39.186: cilium-test/client2-5998d566b4-hrhrb:42274 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 http-request DROPPED (HTTP/1.1 GET http://echo-same-node:8080/foo) Dec 18 14:50:39.186: cilium-test/client2-5998d566b4-hrhrb:42274 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK, FIN) Dec 18 14:50:39.187: cilium-test/client2-5998d566b4-hrhrb:42274 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK)

我们使用如下内容作为 Envoy 的配置文件,其中包含 rewrite 策略。

apiVersion: cilium.io/v2alpha1 kind: CiliumEnvoyConfig metadata:   name: envoy-lb-listener spec:   services:     - name: echo-other-node       namespace: cilium-test     - name: echo-same-node       namespace: cilium-test   resources:     - "@type": type.googleapis.com/envoy.config.listener.v3.Listener       name: envoy-lb-listener       filter_chains:         - filters:             - name: envoy.filters.network.http_connection_manager               typed_config:                 "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager                 stat_prefix: envoy-lb-listener                 rds:                   route_config_name: lb_route                 http_filters:                   - name: envoy.filters.http.router     - "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration       name: lb_route       virtual_hosts:         - name: "lb_route"           domains: ["*"]           routes:             - match:                 prefix: "/"               route:                 weighted_clusters:                   clusters:                     - name: "cilium-test/echo-same-node"                       weight: 50                     - name: "cilium-test/echo-other-node"                       weight: 50                 retry_policy:                   retry_on: 5xx                   num_retries: 3                   per_try_timeout: 1s                 regex_rewrite:                   pattern:                     google_re2: {}                     regex: "^/foo.*$"                   substitution: "/"     - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster       name: "cilium-test/echo-same-node"       connect_timeout: 5s       lb_policy: ROUND_ROBIN       type: EDS       outlier_detection:         split_external_local_origin_errors: true         consecutive_local_origin_failure: 2     - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster       name: "cilium-test/echo-other-node"       connect_timeout: 3s       lb_policy: ROUND_ROBIN       type: EDS       outlier_detection:         split_external_local_origin_errors: true         consecutive_local_origin_failure: 2 

测试请求时,发现可以正确的得到响应了。

➜  cilium-mesh kubectl exec -it -n cilium-test $CLIENT2 -- curl -X GET -I echo-same-node:8080/  HTTP/1.1 200 OK x-powered-by: Express vary: Origin, Accept-Encoding access-control-allow-credentials: true accept-ranges: bytes cache-control: public, max-age=0 last-modified: Sat, 26 Oct 1985 08:15:00 GMT etag: W/"809-7438674ba0" content-type: text/html; charset=UTF-8 content-length: 2057 date: Sat, 18 Dec 2021 15:00:01 GMT x-envoy-upstream-service-time: 1 server: envoy 

并且请求 /foo地址时,也可以正确的得到响应了。

➜  cilium-mesh kubectl exec -it -n cilium-test $CLIENT2 -- curl -X GET -I echo-same-node:8080/foo HTTP/1.1 200 OK x-powered-by: Express vary: Origin, Accept-Encoding access-control-allow-credentials: true accept-ranges: bytes cache-control: public, max-age=0 last-modified: Sat, 26 Oct 1985 08:15:00 GMT etag: W/"809-7438674ba0" content-type: text/html; charset=UTF-8 content-length: 2057 date: Sat, 18 Dec 2021 15:01:40 GMT x-envoy-upstream-service-time: 2 server: envoy 

同时:请求 /foo 的时候,流量如下: 直接转换成功了对/的访问

Dec 18 15:02:22.541: cilium-test/client2-5998d566b4-hrhrb:38860 -> kube-system/coredns-78fcd69978-2ww28:53 L3-L4 REDIRECTED (UDP) Dec 18 15:02:22.541: cilium-test/client2-5998d566b4-hrhrb:38860 -> kube-system/coredns-78fcd69978-2ww28:53 to-proxy FORWARDED (UDP) Dec 18 15:02:22.541: cilium-test/client2-5998d566b4-hrhrb:38860 -> kube-system/coredns-78fcd69978-2ww28:53 to-proxy FORWARDED (UDP) Dec 18 15:02:22.541: cilium-test/client2-5998d566b4-hrhrb:38860 -> kube-system/coredns-78fcd69978-2ww28:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. AAAA) Dec 18 15:02:22.541: cilium-test/client2-5998d566b4-hrhrb:38860 -> kube-system/coredns-78fcd69978-2ww28:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. A) Dec 18 15:02:22.542: cilium-test/client2-5998d566b4-hrhrb:53062 -> cilium-test/echo-same-node:8080 none REDIRECTED (TCP Flags: SYN) Dec 18 15:02:22.542: cilium-test/client2-5998d566b4-hrhrb:53062 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: SYN) Dec 18 15:02:22.542: cilium-test/client2-5998d566b4-hrhrb:53062 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK) Dec 18 15:02:22.542: cilium-test/client2-5998d566b4-hrhrb:53062 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 15:02:22.542: cilium-test/client2-5998d566b4-hrhrb:53048 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 15:02:22.542: cilium-test/client2-5998d566b4-hrhrb:53048 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 http-request FORWARDED (HTTP/1.1 GET http://echo-same-node:8080/) Dec 18 15:02:22.543: cilium-test/client2-5998d566b4-hrhrb:53062 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK, FIN) Dec 18 15:02:22.544: cilium-test/client2-5998d566b4-hrhrb:53062 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK) 

多次请求看日志:

Dec 18 15:07:20.883: cilium-test/client2-5998d566b4-hrhrb:49656 -> kube-system/coredns-78fcd69978-2ww28:53 L3-L4 REDIRECTED (UDP) Dec 18 15:07:20.883: cilium-test/client2-5998d566b4-hrhrb:49656 -> kube-system/coredns-78fcd69978-2ww28:53 to-proxy FORWARDED (UDP) Dec 18 15:07:20.883: cilium-test/client2-5998d566b4-hrhrb:49656 -> kube-system/coredns-78fcd69978-2ww28:53 to-proxy FORWARDED (UDP) Dec 18 15:07:20.883: cilium-test/client2-5998d566b4-hrhrb:49656 -> kube-system/coredns-78fcd69978-2ww28:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. A) Dec 18 15:07:20.884: cilium-test/client2-5998d566b4-hrhrb:49656 -> kube-system/coredns-78fcd69978-2ww28:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. AAAA) Dec 18 15:07:20.885: cilium-test/client2-5998d566b4-hrhrb:53070 -> cilium-test/echo-same-node:8080 none REDIRECTED (TCP Flags: SYN) Dec 18 15:07:20.885: cilium-test/client2-5998d566b4-hrhrb:53070 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: SYN) Dec 18 15:07:20.885: cilium-test/client2-5998d566b4-hrhrb:53070 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK) Dec 18 15:07:20.885: cilium-test/client2-5998d566b4-hrhrb:53070 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 15:07:20.885: cilium-test/client2-5998d566b4-hrhrb:53064 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 15:07:20.885: cilium-test/client2-5998d566b4-hrhrb:53064 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 http-request FORWARDED (HTTP/1.1 GET http://echo-same-node:8080/) Dec 18 15:07:20.886: cilium-test/client2-5998d566b4-hrhrb:53070 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK, FIN) Dec 18 15:07:20.886: cilium-test/client2-5998d566b4-hrhrb:53070 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK) Dec 18 15:07:26.086: cilium-test/client2-5998d566b4-hrhrb:53048 -> cilium-test/echo-same-node-745bd5c77-zpzdn:8080 to-proxy FORWARDED (TCP Flags: ACK)  Dec 18 15:07:44.739: cilium-test/client2-5998d566b4-hrhrb:39057 -> kube-system/coredns-78fcd69978-7lbwh:53 L3-L4 REDIRECTED (UDP) Dec 18 15:07:44.739: cilium-test/client2-5998d566b4-hrhrb:39057 -> kube-system/coredns-78fcd69978-7lbwh:53 to-proxy FORWARDED (UDP) Dec 18 15:07:44.740: cilium-test/client2-5998d566b4-hrhrb:39057 -> kube-system/coredns-78fcd69978-7lbwh:53 to-proxy FORWARDED (UDP) Dec 18 15:07:44.740: cilium-test/client2-5998d566b4-hrhrb:39057 -> kube-system/coredns-78fcd69978-7lbwh:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. AAAA) Dec 18 15:07:44.740: cilium-test/client2-5998d566b4-hrhrb:39057 -> kube-system/coredns-78fcd69978-7lbwh:53 dns-request FORWARDED (DNS Query echo-same-node.cilium-test.svc.cluster.local. A) Dec 18 15:07:44.741: cilium-test/client2-5998d566b4-hrhrb:53072 -> cilium-test/echo-same-node:8080 none REDIRECTED (TCP Flags: SYN) Dec 18 15:07:44.741: cilium-test/client2-5998d566b4-hrhrb:53072 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: SYN) Dec 18 15:07:44.741: cilium-test/client2-5998d566b4-hrhrb:53072 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK) Dec 18 15:07:44.741: cilium-test/client2-5998d566b4-hrhrb:53072 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 15:07:44.742: cilium-test/client2-5998d566b4-hrhrb:53068 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Dec 18 15:07:44.742: cilium-test/client2-5998d566b4-hrhrb:53068 -> cilium-test/echo-other-node-f4d46f75b-bqpcb:8080 http-request FORWARDED (HTTP/1.1 GET http://echo-same-node:8080/) Dec 18 15:07:44.744: cilium-test/client2-5998d566b4-hrhrb:53072 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK, FIN) Dec 18 15:07:44.744: cilium-test/client2-5998d566b4-hrhrb:53072 -> cilium-test/echo-same-node:8080 to-proxy FORWARDED (TCP Flags: ACK)

可以看到它真的成功的进行了负载均衡。

总结

本文我带你部署了 Cilium Service Mesh,并通过两个示例,带你体验了 Cilium Service Mesh 的工作情况。

整体而言, 这种方式能带来一定的便利性,但它的服务间流量配置主要依靠于 CiliumEnvoyConfig ,不算太方便。

一起来期待它后续的演进!


欢迎订阅我的文章公众号【MoeLove】

TheMoeLove

go-zero分布式事务实践

Posted: 19 Dec 2021 04:47 PM PST

背景

随着业务的快速发展、业务复杂度越来越高,微服务作为最佳解决方案之一,它解耦服务,降低复杂度,增加可维护性的同时,也带来一部分新问题。

当我们需要跨服务保证数据一致性时,原先的数据库事务力不从心,无法将跨库、跨服务的多个操作放在一个事务中。这样的应用场景非常多,我们可以列举出很多:

  • 跨行转账场景,数据不在一个数据库,但需要保证余额扣减和余额增加要么同时成功,要么同时失败
  • 发布文章后,更新文章总数等统计信息。其中发布文章和更新统计信息通常在不同的微服务中
  • 微服务化之后的订单系统
  • 出行旅游需要在第三方系统同时定几张票

面对这些本地事务无法解决的场景,我们需要分布式事务的解决方案,保证跨服务、跨数据库更新数据的一致性。

go-zero与dtm强强联合,推出了在go-zero中无缝接入dtm的极简方案,让分布式事务的使用从未如此简单。

运行一个例子

我们来看一个可运行的例子,然后再看如何自己开发完成一个完整的分布式事务

下面以etcd作为注册服务中心,可以按照如下步骤运行一个go-zero的示例:

  • 配置dtm

    MicroService:   Driver: 'dtm-driver-gozero' # 配置dtm使用go-zero的微服务协议   Target: 'etcd://localhost:2379/dtmservice' # 把dtm注册到etcd的这个地址   EndPoint: 'localhost:36790' # dtm的本地地址
  • 启动etcd

    # 前提:已安装etcd etcd
  • 启动dtm

    # 前提:已配置好dtm的数据库链接 go run app/main.go dev
  • 运行一个go-zero的服务

    git clone github.com/yedf/dtmdriver-clients && cd dtmdriver-clients cd gozero/trans && go run trans.go
  • 用go-zero发起一个dtm的事务

    # 在dtmdriver-clients的目录下 cd gozero/app && go run main.go

当您在trans的日志中看到

2021/12/03 15:44:05 transfer out 30 cents from 1 2021/12/03 15:44:05 transfer in 30 cents to 2 2021/12/03 15:44:05 transfer out 30 cents from 1 2021/12/03 15:44:05 transfer out 30 cents from 1

那就是事务正常完成了

开发接入

参考yedf/dtmdriver-clients的代码

// 下面这行导入gozero的dtm驱动 import _ "github.com/yedf/dtmdriver-gozero"  // 使用dtm的客户端dtmgrpc之前,需要执行下面这行调用,告知dtmgrpc使用gozero的驱动来如何处理gozero的url err := dtmdriver.Use("dtm-driver-gozero") // check err  // dtm已经通过前面的配置,注册到下面这个地址,因此在dtmgrpc中使用该地址 var dtmServer = "etcd://localhost:2379/dtmservice"  // 下面从配置文件中Load配置,然后通过BuildTarget获得业务服务的地址 var c zrpc.RpcClientConf conf.MustLoad(*configFile, &c) busiServer, err := c.BuildTarget()    // 使用dtmgrpc生成一个消息型分布式事务并提交     gid := dtmgrpc.MustGenGid(dtmServer)     msg := dtmgrpc.NewMsgGrpc(dtmServer, gid).     // 事务的第一步为调用trans.TransSvcClient.TransOut     // 可以从trans.pb.go中找到上述方法对应的Method名称为"/trans.TransSvc/TransOut"     // dtm需要从dtm服务器调用该方法,所以不走强类型,而是走动态的url: busiServer+"/trans.TransSvc/TransOut"         Add(busiServer+"/trans.TransSvc/TransOut", &busi.BusiReq{Amount: 30, UserId: 1}).         Add(busiServer+"/trans.TransSvc/TransIn", &busi.BusiReq{Amount: 30, UserId: 2})     err := msg.Submit() 

整个开发接入的过程很少,前面的注释已经很清晰,就不再赘述了

注意事项

在开发接入的过程中,去找*.pb.go的文件中的grpc访问的方法路径时候,一定要找invoke的路径

image.png

image.png

深入理解动态调用

在go-zero使用dtm的分布式事务时,许多的调用是从dtm服务器发起的,例如TCC的Confirm/Cancel,SAGA/MSG的所有调用。

dtm无需知道组成分布式事务的相关业务api的强类型,它是动态的调用这些api。

grpc的调用,可以类比于HTTP的POST,其中:

  • c.BuildTarget() 产生的target类似于URL中的Host
  • "/trans.TransSvc/TransOut" 相当于URL中的Path
  • &busi.BusiReq{Amount: 30, UserId: 1} 相当于Post中Body
  • pb.Response 相当于HTTP请求的响应

通过下面这部分代码,dtm就拿到了完整信息,就能够发起完整的调用了

Add(busiServer+"/trans.TransSvc/TransOut", &busi.BusiReq{Amount: 30, UserId: 1})

更加完整的例子

热心的社区同学Mikael帮忙写了一个内容更加丰富的例子,结合实际应用和子事务屏障,完整的演示了一个线上实际运行的分布式事务,有兴趣的同学可以参考:

https://github.com/Mikaelemmmm/gozerodtm

其他方式接入

go-zero的微服务还有非etcd的其他方式,我们依次说明他们的接入方式

直连

对于直连这种方式,您只需要在上面dtm的etcd配置基础上,将Target设置为空字符串即可。

直连的情况,不需要将dtm注册到注册中心

K8S

对于K8S这种方式,您只需要在上面dtm的etcd配置基础上,将Target设置为空字符串即可。

在K8S中,将服务注册到K8S中,是由deployment.yaml完成的,应用内部,不需要进行注册

直播分享预告

go-zero的作者和我(dtm的作者)将在12月22日晚21点,在talkgo,联合做一场《go-zero的分布式事务实践》的直播分享,将会带来更多更深入的讨论。欢迎大家届时参加。

直播地址为:https://live.bilibili.com/111...

小结

这一次go-zero与dtm的合作,在go生态中,打造了首个原生支持分布式事务的微服务解决方案,意义重大。

欢迎大家使用我们的go-zerodtm,使用我们原生的"分布式事务的微服务解决方案",并star支持我们

面试官:要不我们聊一下“心跳”的设计?

Posted: 13 Dec 2021 08:23 PM PST

你好呀,我是歪歪。

是这样的,我最近又看到了这篇文章《工商银行分布式服务 C10K 场景解决方案
》。

为什么是又呢?

因为这篇文章最开始发布的时候我就看过了,当时就觉得写得挺好的,宇宙行(工商银行)果然是很叼的样子。

但是看过了也就看过了,当时没去细琢磨。

这次看到的时候,刚好是在下班路上,就仔仔细细的又看了一遍。

嗯,常读常新,还是很有收获的。

所以写篇文章,给大家汇报一下我再次阅读之后的一下收获。

文章提要

我知道很多同学应该都没有看过这篇文章,所以我先放个链接,[《工商银行分布式服务 C10K 场景解决方案
》](https://mp.weixin.qq.com/s/qc...)。

先给大家提炼一下文章的内容,但是如果你有时间的话,也可以先去细细的读一下这篇文章,感受一下宇宙行的实力。

文章内容大概是这样的。

在宇宙行的架构中,随着业务的发展,在可预见的未来,会出现一个提供方为数千个、甚至上万个消费方提供服务的场景。

在如此高负载量下,若服务端程序设计不够良好,网络服务在处理数以万计的客户端连接时、可能会出现效率低下甚至完全瘫痪的情况,即为 C10K 问题。

C10K 问题就不展开讲了,网上查一下,非常著名的程序相关问题,只不过该问题已经成为历史了。

而宇宙行的 RPC 框架使用的是 Dubbo,所以他们那篇文章就是基于这个问题去展开的:

基于 Dubbo 的分布式服务平台能否应对复杂的 C10K 场景?

为此,他们搭建了大规模连接环境、模拟服务调用进行了一系列探索和验证。

首先他们使用的 Dubbo 版本是 2.5.9。版本确实有点低,但是银行嘛,懂的都懂,架构升级是能不动就不动,稳当运行才是王道。

在这个版本里面,他们搞了一个服务端,服务端的逻辑就是 sleep 100ms,模拟业务调用,部署在一台 8C16G 的服务器上。

对应的消费方配置服务超时时间为 5s,然后把消费方部署在数百台 8C16G 的服务器上(我滴个乖乖,数百台 8C16G 的服务器,这都是白花花的银子啊,有钱真好),以容器化方式部署 7000 个服务消费方。

每个消费方启动后每分钟调用 1 次服务。

然后他们定制了两个测试的场景:

.png)

场景 2 先暂时不说,异常是必然的,因为只有一个提供方嘛,重启期间消费方还在发请求,这必然是要凉的。

但是场景 1 按理来说不应该的啊。

你想,消费方配置的超时时间是 5s,而提供方业务逻辑只处理 100ms。再怎么说时间也是够够的了。

需要额外多说一句的是:本文也只聚焦于场景 1。

但是,朋友们,但是啊。

虽然调用方一分钟发一次请求的频率不高,但是架不住调用方有 7000 个啊,这 7000 个调用方,这就是传说中的突发流量,只是这个"突发"是每分钟一次。

所以,偶现超时也是可以理解的,毕竟服务端处理能力有限,有任务在队列里面稍微等等就超时了。

可以写个小例子示意一下,是这样的:

就是搞个线程池,线程数是 200。然后提交 7000 个任务,每个任务耗时 100ms,用 CountDownLatch 模拟了一下并发,在我的 12 核的机器上运行耗时 3.8s 的样子。

也就是说如果在 Dubbo 的场景下,每一个请求再加上一点点网络传输的时间,一点点框架内部的消耗,这一点点时间再乘以 7000,最后被执行的任务理论上来说,是有可能超过 5s 的。

所以偶现超时是可以理解的。

但是,朋友们,又来但是了啊。

我前面都说的是理论上,然而实践才是检验真理的唯一办法。

看一下宇宙行的验证结果:

首先我们可以看到消费方不论是发起请求还是处理响应都是非常迅速的,但是卡壳就卡在服务方从收到请求到处理请求之间。

经过抓包分析,他们得出结论:导致交易超时的原因不在消费方侧,而在提供方侧。

这个结论其实也很好理解,因为压力都在服务提供方这边,所以阻塞也应该是在它这里。

其实到这里我们基本上就可以确认,肯定是 Dubbo 框架里面的某一些操作导致了耗时的增加。

难的就是定位到,到底是什么操作呢?

宇宙行通过一系列操作,经过缜密的分析,得出了一个结论:

心跳密集导致 netty worker 线程忙碌,从而导致交易耗时增长。

也就是结论中提到的这一点:

有了结论,找到了病灶就好办了,对症下药嘛。

因为前面说过,本文只聚焦于场景一,所以我们看一下对于场景一宇宙行给出的解决方案:

全都是围绕着心跳的优化处理,处理完成后的效果如下:

其中效果最显著的操作是"心跳绕过序列化"。

消费方与提供方之间平均处理时差由 27ms 降低至 3m,提升了 89%。

前 99% 的交易耗时从 191ms 下降至 133ms,提升了 30%。

好了,写到这,就差不多是把那篇文章里面我当时看到的一些东西复述了一遍,没啥大营养。

只是我还记得第一次看到这篇文章的时候,我是这样的:

我觉得挺牛逼的,一个小小的心跳,在 C10K 的场景下竟然演变成了一个性能隐患。

我得去研究一下,顺便宇宙行给出的方案中最重要的是"心跳绕过序列化",我还得去研究一下 Dubbo 怎么去实现这个功能,研究明白了这玩意就是我的了啊。

但是...

我忘记当时为啥没去看了,但是没关系,我现在想起来了嘛,马上就开始研究。

心跳如何绕过序列化

我是怎么去研究呢?

直接往源码里面冲吗?

是的,就是往源码里面冲。

但是冲之前,我先去 Dubb 的 github 上逛了一圈:

https://github.com/apache/dubbo

然后在 Pull request 里面先搜索了一下"Heartbeat",这一搜还搜出不少好东西呢:

我一眼看到这两个 pr 的时候,眼睛都在放光。

好家伙,我本来只是想随便看看,没想到直接定位了我要研究的东西了。

我只需要看看这两个 pr,就知道是怎么实现的"心跳绕过序列化",这直接就让我少走了很多弯路。

首先看这个:

https://github.com/apache/dub...

从这段描述中可以知道,我找到对的地方了。而从他的描述中知道"心跳跳过序列化",就是用 null 来代替了序列化的这个过程。

同时这个 pr 里面还说明了自己的改造思路:

接着就带大家看一下这一次提交的代码。

怎么看呢?

可以在 git 上看到他对应这次提交的文件:

到源码里面找到对应地方即可,这也是一个去找源码的方法。

我比较熟悉 Dubbo 框架,不看这个 pr 我也大概知道去哪里找对应的代码。但是如果换成另外一个我不熟悉的框架呢?

从它的 git 入手其实是一个很好的角度。

一个翻阅源码的小技巧,送给你。

如果你不了解 Dubbo 框架也没有关系,我们只是聚焦于"心跳是如何跳过序列化"的这一个点。至于心跳是由谁如何在什么时间发起的,这一节暂时不讲。

接着,我们从这个类下手:

org.apache.dubbo.rpc.protocol.dubbo.DubboCodec

从提交记录可以看出主要有两处改动,且两处改动的代码是一模一样的,都位于 decodeBody 这个方法,只是一个在 if 分支,一个在 else 分支:

这个代码是干啥的?

你想一个 RPC 调用,肯定是涉及到报文的 encode(编码) 和 decode(解码) 的,所以这里主要就是对请求和响应报文进行 decode 。

一个心跳,一来一回,一个请求,一个响应,所以有两处改动。

所以我带着大家看请求包这一处的处理就行了:

可以看到代码改造之后,对心跳包进行了一个特殊的判断。

在心跳事件特殊处理里面涉及到两个方法,都是本次提交新增的方法。

第一个方法是这样的:

org.apache.dubbo.remoting.transport.CodecSupport#getPayload

就是把 InputStream 流转换成字节数组,然后把这个字节数组作为入参传递到第二个方法中。

第二个方法是这样的:

org.apache.dubbo.remoting.transport.CodecSupport#isHeartBeat

从方法名称也知道这是判断请求是不是心跳包。

怎么去判断它是心跳包呢?

首先得看一下发起心跳的地方:

org.apache.dubbo.remoting.exchange.support.header.HeartbeatTimerTask#doTask

从发起心跳的地方我们可以知道,它发出去的东西就是 null。

所以在接受包的地方,判断其内容是不是 null,如果是,就说明是心跳包。

通过这简单的两个方法,就完成了心跳跳过序列化这个操作,提升了性能。

而上面两个方法都是在这个类中,所以核心的改动还是在这个类,但是改动点其实也不算多:

org.apache.dubbo.remoting.transport.CodecSupport

在这个类里面有两个小细节,可以带大家再看看。

首先是这里:

这个 map 里面缓存的就是不同的序列化的方式对应的 null,代码干的也就是作者这里说的这件事儿:

另外一个细节是看这个类的提交记录:

还有一次优化性的提交,而这一次提交的内容是这样的。

首先定义了一个 ThreadLocal,并使其初始化的时候是 1024 字节:

那么这个 ThreadLocal 是用在哪儿的呢?

在读取 InputStream 的时候,需要开辟一个字节数组,为了避免频繁的创建和销毁这个字节数据,所以搞了一个 ThreadLocal:

有的同学看到这里就要问了:为什么这个 ThreadLocal 没有调用 remove 方法呢,不会内存泄漏嘛?

不会的,朋友们,在 Dubbo 里面执行这个玩意的是 NIO 线程,这个线程是可以复用的,且里面只是放了一个 1024 的字节数组,不会有脏数据,所以不需要移除,直接复用。

正是因为可以复用,所以才提升了性能。

这就是细节,魔鬼都在细节里面。

这一处细节,就是前面提到的另外一个 pr:

https://github.com/apache/dub...

看到这里,我们也就知道了宇宙行到底是怎么让心跳跳过序列化操作了,其实也没啥复杂的代码,几十行代码就搞定了。

但是,朋友们,又要但是了。

写到这里的时候,我突然感觉到不太对劲。

因为我之前写过这篇文章,Dubbo 协议那点破事

在这篇文章里面有这样的一个图:

这是当时在官网上截下来的。

在协议里面,事件标识字段之前只有 0 和 1。

但是现在不一样了,从代码看,是把 1 的范围给扩大了,它不一定代表的是心跳,因为这里面有个 if-else

所以,我就去看了一下现在官网上关于协议的描述。

https://dubbo.apache.org/zh/d...

果然,发生了变化:

并不是说 1 就是心跳包,而是改口为:1 可能是心跳包。

严谨,这就是严谨。

所以开源项目并不是代码改完就改完了,还要考虑到一些周边信息的维护。

心跳的多种设计方案

在研究 Dubbo 心跳的时候,我还找到了这样一个 pr。

https://github.com/apache/dub...

标题是这样的:

翻译过来就是使用 IdleStateHandler 代替使用 Timer 发送心跳的建议。

我定睛一看,好机会,这不是 95 后老徐嘛,老熟人了。

看一下老徐是怎么说的,他建议具体是这样的:

几位 Dubbo 大佬,在这个 pr 里面交换了很多想法,我仔细的阅读之后都受益匪浅。

大家也可以点进去看看,我这里给大家汇报一下自己的收获。

首先是几位老哥在心跳实时性上的一顿 battle。

总之,大家知道 Dubbo 的心跳检测是有一定延时的,因为是基于时间轮做的,相当于是定时任务,触发的时效性是不能保证实时触发的。

这玩意就类似于你有一个 60 秒执行一次的定时任务,在第 0 秒的时候任务启动了,在第 1 秒的时候有一个数据准备好了,但是需要等待下一次任务触发的时候才会被处理。因此,你处理数据的最大延迟就应该是 60 秒。

这个大家应该能明白过来。

额外说一句,上面讨论的结果是"目前是 1/4 的 heartbeat 延时",但是我去看了一下最新的 master 分支的源码,怎么感觉是 1/3 的延时呢:

从源码里可以看到,计算时间的时候 HEARTBEAT_CHECK_TICK 参数是 3。所以我理解是 1/3 的延时。

但是不重要,这不重要,反正你知道是有延时的就行了。

而 kexianjun 老哥认为如果基于 netty 的 IdleStateHandler 去做,每次检测超时都重新计算下一次检测的时间,因此相对来说就能比较及时的检查到超时了。

这是在实时性上的一个优化。

而老徐觉得,除了实时性这个考虑外,其实 IdleStateHandler 更是一个针对心跳的优雅的设计。但是呢,由于是基于 Netty 的,所以当通讯框架使用的不是 Netty 的时候,就回天无力了,所以可以保留 Timer 的设计来应对这种情况。

很快,carryxyh 老哥就给出了很有建设性的意见:

由于 Dubbo 是支持多个通讯框架的。

这里说的"多个",其实不提我都忘记了,除了 Netty 之外,它还支持 Girzzly 和 Mina 这两种底层通讯框架,而且还支持自定义。

但是我寻思都 2021 年了,Girzzly 和 Mina 还有人用吗?

从源码中我们也能找到它们的影子:

org.apache.dubbo.remoting.transport.AbstractEndpoint

Girzzly、Mina 和 Netty 都各有自己的 Server 和 Client。

其中 Netty 有两个版本,是因为 Netty4 步子迈的有点大,难以在之前的版本中进行兼容,所以还不如直接多搞一个实现。

但是不管它怎么变,它都还是叫做 Netty。

好了,说回前面的建设性意见。

如果是采用 IdleStateHandler 的方式做心跳,而其他的通讯框架保持 Timer 的模式,那么势必会出现类似于这样的代码:

if transport == netty {      don't start heartbeat timer }

这是一个开源框架中不应该出现的东西,因为会增加代码复杂度。

所以,他的建议是最好还是使用相同的方式来进行心跳检测,即都用 Timer 的模式。

正当我觉得这个哥们说的有道理的时候,我看了老徐的回答,我又瞬间觉得他说的也很有道理:

我觉得上面不需要我解释了,大家边读边思考就行了。

接着看看 carryxyh 老哥的观点:

这个时候对立面就出现了。

老徐的角度是,心跳肯定是要有的,只是他觉得不同通讯框架的实现方式可以不必保持一致(现在都是基于 Timer 时间轮的方式),他并不认为 Timer 抽象成一个统一的概念去实现连接保活是一个优雅的设计。

在 Dubbo 里面我们主要用的就是 Netty,而 Netty 的 IdleStateHandler 机制,天生就是拿来做心跳的。

所以,我个人认为,是他首先觉得使用 IdleStateHandler 是一种比较优雅的实现方式,其次才是时效性的提升。

但是 carryxyh 老哥是觉得 Timer 抽象的这个定时器,是非常好的设计,因为它的存在,我们才可以不关心底层是netty还是mina,而只需要关心具体实现。

而对于 IdleStateHandler 的方案,他还是认为在时效性上有优势。但是我个人认为,他的想法是如果真的有优势的话,我们可以参考其实现方式,给其他通讯框架也赋能一个 "Idle" 的功能,这样就能实现大统一。

看到这里,我觉得这两个老哥 battle 的点是这样的。

首先前提是都围绕着"心跳"这个功能。

一个认为当使用 Netty 的时候"心跳"有更好的实现方案,且 Netty 是 Dubbo 主要的通讯框架,所以应该可以只改一下 Netty 的实现。

一个认为"心跳"的实现方案应该统一,如果 Netty 的 IdleStateHandler 方案是个好方案,我们应该把这个方案拿过来。

我觉得都有道理,一时间竟然不知道给谁投票。

但是最终让我选择投老徐一票的,是看了他写的这篇文章:《一种心跳,两种设计》

这篇文章里面他详细的写了 Dubbo 心跳的演变过程,其中也涉及到部分的源码。

最终他给出了这样的一个图, 心跳设计方案对比:

然后,是这段话:

.png)

老徐是在阿里搞中间件的,原来搞中间件的人每天想的是这些事情。

有点意思。

看看代码

带大家看一下代码,但是不会做详细分析,相当于是指个路,如果想要深入了解的话,自己翻源码去。

首先是这里:

org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeClient

可以看到在 HeaderExchangeClient 的构造方法里面调用了 startHeartBeatTask 方法来开启心跳。

同时这里面有个 HashedWheelTimer,这玩意我熟啊,时间轮嘛,之前分析过的。

然后我们把目光放在这个方法 startHeartBeatTask:

这里面就是构建心跳任务,然后扔到时间轮里面去跑,没啥复杂的逻辑。

这一个实现,就是 Dubbo 对于心跳的默认处理。

但是需要注意的是,整个方法被 if 判断包裹了起来,这个判断可是大有来头,看名字叫做 canHandleIdle,即是否可以处理 idle 操作,默认是 false:

所以,前面的 if 判断的结果是 true。

那么什么情况下 canHandleIdle 是 true 呢?

在使用 Netty4 的时候是 true。

也就是 Netty4 不走默认的这套心跳实现。

那么它是怎么实现的呢?

由于服务端和客户端的思路是一样的,所以我们看一下客户端的代码就行。

关注一下它的 doOpen 方法:

org.apache.dubbo.remoting.transport.netty4.NettyClient#doOpen

在 pipeline 里面加入了我们前面说到的 IdleStateHandler 事件,这个事件就是如果 heartbeatInterval 毫秒内没有读写事件,那么就会触发一个方法,相当于是一个回调。

heartbeatInterval 默认是 6000,即 60s。

然后加入了 nettyClientHandler,它是干什么呢?

看一眼它的这个方法:

org.apache.dubbo.remoting.transport.netty4.NettyClientHandler#userEventTriggered

这个方法里面在发送心跳事件。

也就是说你这样写,含义是在 60s 内,客户端没有发生读写时间,那么 Netty 会帮我们触发 userEventTriggered 方法,在这个方法里面,我们可以发送一次心跳,去看看服务端是否正常。

从目前的代码来看, Dubbo 最终是采用的老徐的建议,但是默认实现还是没变,只是在 Netty4 里面采用了 IdleStateHandler 机制。

这样的话,其实我就觉得更奇怪了。

同样是 Netty,一个采用的是时间轮,一个采用的 IdleStateHandler。

同时我也很理解,步子不能迈的太大了,容易扯着蛋。

但是,在翻源码的过程中,我发现了一个代码上的小问题。

org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, int, byte[])

在上面这个方法中,有两行代码是这样的:

你先别管它们是干啥的,我就带你看看它们的逻辑是怎么样的:

可以看到两个方法都执行了这样的逻辑:

int payload = getPayload(channel); boolean overPayload = isOverPayload(payload, size);

如果 finishRespWhenOverPayload 返回的不是 null,没啥说的,返回 return 了,不会执行 checkPayload 方法。

如果 finishRespWhenOverPayload 返回的是 null,则会执行 checkPayload 方法。

这个时候会再次做检查报文大小的操作,这不就重复了吗?

所以,我认为这一行的代码是多余的,可以直接删除。

你明白我意思吧?

又是一个给 Dubbo 贡献源码的机会,送给你,可以冲一波。

最后,再给大家送上几个参考资料。

第一个是可以去了解一下 SOFA-RPC 的心跳机制。 SOFA-PRC 也是阿里开源出来的框架。

在心跳这块的实现就是完完全全的基于 IdleStateHandler 来实现的。

可以去看一下官方提供的这两篇文章:

https://www.sofastack.tech/se...

第二个是极客时间《从0开始学微服务》,第 17 讲里面,老师在关于心跳这块的一点分享,提到的一个保护机制,这是我之前没有想到过的:

反正我是觉得,我文章中提到的这一些链接,你都去仔仔细细的看了,那么对于心跳这块的东西,也就掌握的七七八八了,够用了。

好了,就到这吧。

本文已收录至个人博客,欢迎大家来玩。

https://www.whywhy.vip/

No comments:

Post a Comment