• 还在篮子里

    分布式应用运行时 Dapr X Kubernetes · 语雀

    分布式应用运行时 Dapr X Kubernetes

    前言

    我前不久体验了 Dapr 的单机使用,体验不错。不过Dapr 作为一个分布式应用的运行时,当然需要加入集群使用才能完全体验呀。我们使用 GitHub 上的文档 部署。首先我们需要一个 Kubernetes 集群来做实验,选择 Rancher 由于我比较熟悉 Rancher ,同时 UI 操作才能让人感受到工具的便利嘛。之后会使用 RancherHelm 部署 DaprRedis 图表,不过由于 UI 操作的便利,点一下就部署了。

    集群的环境部署比较长,不过本来这些工具就是部署之后一劳永逸降低心智负担,当然你有一个 Kubernetes 可以直接实验就更好了。

    下面是 架构 图

    image.png

    可以看到我们已经接触到了 Dapr 多语言的特性了。两个 Pod 包括不同语言的应用:Python 和 Node.js ,他们通过 Dapr Injects 的 sidecar 交互。

    那么我们开始吧!

    环境

    Kubernetes 部署

    Docker

    1$ yum install docker-ce-18.06.2.ce-3.el7 -y
    此处为语雀文档,点击链接查看:https://www.yuque.com/opte0v/shu93f/gss3ca

    Rancher

    1$ sudo docker run -d --restart=unless-stopped -p 80:80 -p 443:443 rancher/rancher:latest
    此处为语雀文档,点击链接查看:https://www.yuque.com/opte0v/shu93f/td5aq3

    K8S By Rancher

    此处为语雀文档,点击链接查看:https://www.yuque.com/opte0v/shu93f/k8sbyrancher

    你也可以使用其他的工具快速启动 Kubernetes 集群

    可选部署 Kubernetes 方法

    Dapr

    1. Using Helm(Advanced)

    添加 Dapr 源 https://daprio.azurecr.io/helm/v1/repo 到商店

    image.png

    1. 启动 Dapr

    image.png

    1. 默认配置即可

    image.png

    1. 等待一段时间完成

    image.png

    你可以选择其他方法安装 Dapr

    Using Dapr CLI【可选】官方文档

    Redis

    1. 同样在 Rancher 的商店中找到 Redis Chart,默认部署即可。(可以自己设定一些选项比如密码和访问端口等方便你接下来的操作)

    image.png

    1. 等待一会儿部署完毕

    image.png

    1. 使用 Rancher 自带的 kubectl 即可执行命令

    image.png

    1. 使用 以下命令获取到 redis 命名空间下设置的密码

    kubectl get secret --namespace redis redis -o jsonpath="{.data.redis-password}" | base64 --decode

    image.png

    可以看到密码是 123456(方便实验,内网环境,生产环境请使用生成密码)

    1. 找到 Redis 的访问端口,我的是 192.168.0.116:32091(这是随机的端口)

    image.png

    1. 接下来我们把这些值置换到官方给的模板里的

    Configuration

    这里是明文存储的 secret ,生产环境请参照.secret management .

    Configuring Redis for State Persistence and Retrieval

    在刚才的 kubectl 命令行中继续操作。

    1$ vi redis-state.yaml

    记得将上文 4 ,5 步骤得到的 HOST 地址 和 Password 密码替换.

     1apiVersion: dapr.io/v1alpha1
     2kind: Component
     3metadata:
     4  name: statestore
     5spec:
     6  type: state.redis
     7  metadata:
     8  - name: redisHost
     9    value: <HOST>
    10  - name: redisPassword
    11    value: <PASSWORD>

    同时这个文件也可以在 samples/2.hello-kubernetes/deploy 下找到

    我的 Yaml 文件

    Apply the configuration

    使用 kubectl apply 部署。

    1$ kubectl apply -f redis-state.yaml
    2$ kubectl apply -f redis-pubsub.yaml

    HelloKubernetes

    Download Code

    首先下载示例代码

    1$ git clone https://github.com/dapr/samples.git
    2$ cd samples/2.hello-kubernetes/node

    Node

    该示例有多个应用,首先我们看老朋友 Node.js 的代码

    Cat app.js

     1// $ cat app.js 
     2
     3const express = require('express');
     4const bodyParser = require('body-parser');
     5require('isomorphic-fetch');
     6
     7const app = express();
     8app.use(bodyParser.json());
     9
    10const daprPort = process.env.DAPR_HTTP_PORT || 3500;
    11const daprUrl = `http://localhost:${daprPort}/v1.0`;
    12const port = 3000;
    13
    14app.get('/order', (_req, res) => {
    15    fetch(`${daprUrl}/state/order`)
    16        .then((response) => {
    17            return response.json();
    18        }).then((order) => {
    19            res.send(order);
    20    });
    21});
    22
    23app.post('/neworder', (req, res) => {
    24    const data = req.body.data;
    25    const orderId = data.orderId;
    26    console.log("Got a new order! Order ID: " + orderId);
    27
    28    const state = [{
    29        key: "order",
    30        value: data
    31    }];
    32
    33    fetch(`${daprUrl}/state`, {
    34        method: "POST",
    35        body: JSON.stringify(state),
    36        headers: {
    37            "Content-Type": "application/json"
    38        }
    39    }).then((response) => {
    40        console.log((response.ok) ? "Successfully persisted state" : "Failed to persist state");
    41    });
    42
    43    res.status(200).send();
    44});
    45
    46app.listen(port, () => console.log(`Node App listening on port ${port}!`));

    可以看到这个和我们之前使用的单机的 Node.js 一样,可以参照我的这篇文章

    Node Application Explain

    此处为语雀文档,点击链接查看:https://www.yuque.com/abser/process/fa0ntp#zjdNP

    Deploy Node Application

    虽然代码一样,但是我们部署的方法变了,不是直接使用 dapr 进行部署,而是使用 kubectl(注意我们当前目录还在 samples/2.hello-kubernetes/node

    1$ kubectl apply -f ../deploy/node.yaml
    2
    3service/nodeapp created
    4deployment.apps/nodeapp created

    如果你好奇,我们可以看一看 node.yaml 的内容(我把代码块调小了,可以自己滑动查看)

     1kind: Service
     2apiVersion: v1
     3metadata:
     4  name: nodeapp
     5  labels:
     6    app: node
     7spec:
     8  selector:
     9    app: node
    10  ports:
    11  - protocol: TCP
    12    port: 80
    13    targetPort: 3000
    14  type: LoadBalancer
    15
    16---
    17apiVersion: apps/v1
    18kind: Deployment
    19metadata:
    20  name: nodeapp
    21  labels:
    22    app: node
    23spec:
    24  replicas: 1
    25  selector:
    26    matchLabels:
    27      app: node
    28  template:
    29    metadata:
    30      labels:
    31        app: node
    32      annotations:
    33        dapr.io/enabled: "true"
    34        dapr.io/id: "nodeapp"
    35        dapr.io/port: "3000"
    36    spec:
    37      containers:
    38      - name: node
    39        image: dapriosamples/hello-k8s-node
    40        ports:
    41        - containerPort: 3000
    42        imagePullPolicy: Always
    

    注意这两个选项,    dapr.io/enabled: "true" 告诉 Dapr 控制器注入边车

     dapr.io/id: "nodeapp" 告诉 Dapr 这个部署的唯一 ID(用来 Dapr 之间相互通信)

    现在我们来查看状态和外部 IP(外部 IP 在 pending 是因为我们使用 Rancher 的原因,4 层负载均衡并不是每一个运营商都支持,不过外部 IP 不影响本次 Demo, 我会在下面做一个 Ingress 访问它

    1$ kubectl get svc nodeapp
    2NAME      TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
    3nodeapp   LoadBalancer   10.43.255.238   <pending>     80:30557/TCP   5m17s

    将 NodeApp 导入环境变量【跳过:由于我们没有 EXTERNAL-IP 】

    1$ export NODE_APP=$(kubectl get svc nodeapp --output 'jsonpath={.status.loadBalancer.ingress[0].ip}')

    Python

    我们还有一个 Python 的 Application 需要部署。

    1$cd samples/2.hello-kubernetes/python 
    2$cat app.py

    Python 的代码很简单,他把 JSON 消息发到 localhost:3500 (这个端口是可变的,3500Dapr 默认端口) 调用 Node App 的 neworder endpoint 。我们的消息也是很简单的每秒递增一次的消息。

     1import time
     2import requests
     3import os
     4
     5dapr_port = os.getenv("DAPR_HTTP_PORT", 3500)
     6dapr_url = "http://localhost:{}/v1.0/invoke/nodeapp/method/neworder".format(dapr_port)
     7
     8n = 0
     9while True:
    10    n += 1
    11    message = {"data": {"orderId": n}}
    12
    13    try:
    14        response = requests.post(dapr_url, json=message)
    15    except Exception as e:
    16        print(e)
    17
    18    time.sleep(1)

    部署并等待 Pod 进入 Running 状态

    1$ kubectl apply -f ./deploy/python.yaml
    2$ kubectl get pods --selector=app=python -w
    3
    4NAME                        READY   STATUS              RESTARTS   AGE
    5pythonapp-b5fd4474d-tk84x   0/2     ContainerCreating   0          9s
    6pythonapp-b5fd4474d-tk84x   1/2     ErrImagePull        0          38s
    7pythonapp-b5fd4474d-tk84x   1/2     ImagePullBackOff    0          39s
    8pythonapp-b5fd4474d-tk84x   2/2     Running             0          3m24s

    Observe

    我们毕竟使用的是 UI 界面,现在我们可以通过 Rancher UI 检验一下我们的成果。

    可以看到 nodeapppythonapp 两个 Pod 都在 active 状态。同时我们点进入更多信息界面可以看到 Pod 中还有 Dapr 的 边车

    image.png

    我们进入 nodeapp 检查日志

    image.png

    或者使用命令行

    1$ kubectl logs --selector=app=node -c node
    2
    3Got a new order! Order ID: 1
    4Successfully persisted state
    5Got a new order! Order ID: 2
    6Successfully persisted state
    7Got a new order! Order ID: 3
    8Successfully persisted state

    Persistence

    当然,我们不能使用 LoadBalance 或者 外部 IP 并不能阻止我们检查持久性。

    我们可以通过 Rancher 添加一个 Ingress 七层负载均衡到 目标容器进行访问。

    image.png

    然后设置 nodeapp 的端口 3000

    image.png

    等待一会儿出现分配的地址 http://nodeapp.default.192.168.0.102.xip.io

    image.png

    浏览器或者命令行访问,会返回最新的 orderID

    1$ curl http://nodeapp.default.192.168.0.102.xip.io/order
    2{"orderID":"42"}

    Clean

    这会清理所有的东西,包括 状态组件。

    1$ cd samples/2.hello-kubernetes/deploy
    2$ kubectl delete -f .

    image.png

    Summary

    首先使用 Dapr 的开发感受还需要一步,就是修改代码之后编译为自己的 Docker 然后部署在 Kubernetes 上,这里不详述,留给读者当练手即可。引用官方的 Next Steps 大概导引一下。

    Next Steps

    Now that you’re successfully working with Dapr, you probably want to update the sample code to fit your scenario. The Node.js and Python apps that make up this sample are deployed from container images hosted on a private Azure Container Registry. To create new images with updated code, you’ll first need to install docker on your machine. Next, follow these steps:

    1. Update Node or Python code as you see fit!
    2. Navigate to the directory of the app you want to build a new image for.
    3. Run docker build -t <YOUR_IMAGE_NAME> . . You can name your image whatever you like. If you’re planning on hosting it on docker hub, then it should start with <YOUR_DOCKERHUB_USERNAME>/.
    4. Once your image has built you can see it on your machines by running docker images.
    5. To publish your docker image to docker hub (or another registry), first login: docker login. Then rundocker publish <YOUR IMAGE NAME>.
    6. Update your .yaml file to reflect the new image name.
    7. Deploy your updated Dapr enabled app: kubectl apply -f <YOUR APP NAME>.yaml.

    这一次的 Dapr 体验遇到的一些问题主要是集群本身的网络问题。本身 Dapr 在这个 Demo 中起到的就是通信和存储状态的服务,但是在实际使用中并没有看到非常多的日志可供调试。也就是说依旧需要专业的运维人员进行 Dapr 开发的维护。不过,本来 Dapr 的打算就是制定标准,划开开发人员和运维人员的界限降低心智负担。

    Demo 中是两个语言的应用,我在查看日志的时候看了下环境变量,看到了 GRPC 的 50001 端口。同时 Dapr 也提供 各个语言的 SDK。我们看过了两个应用的代码,都很简单,不过可以看到的是对于通信的感知是不大的,或者说对于 Dapr 的感知不大,意味着微服务改为 Dapr 的并不需要改动太多,就是服务的 URL 变动一下。剩余的交给 Dapr 的 Sidecar 就行了。

    如果你不选择 Rancher 作为 搭建 Kubernetes 的集群的工具的话,跳转 也有其他的教程指导。没有一些非常明白好看的 UI 了。Rancher 的可视化部署状态在这个 Demo 中起到了很大的作用。

    接下来对于 Dapr 会尝试一下 他的事件驱动特性和 Pub/Sub 。这对于分布式应用来说也比较好玩。我非常赞同边车的开发模式,说到 sidecar 很多人都会想到 Istio 和 envoy,我也是一开始因为 ServiceMesh 和 sidecar 才开始关注 Dapr 的。刚看到开源的消息到现在,GitHub stars 已经到 4k 了,不过一天时间涨了 1k 多。微软的能量还是挺大的。

    Refer

    来源: 分布式应用运行时 Dapr X Kubernetes · 语雀

  • 还在篮子里

    Dapr HelloWorld · 语雀

    Dapr HelloWorld

    Dapr

    Distributed Application Runtime. An event-driven, portable runtime for building microservices on cloud and edge.

    分布式应用运行时、事件驱动、为云和边缘构建微服务提供便携化运行时。

    我现在也不是很懂。

    dapr/dapr

    GitHub

    Dapr is a portable, serverless, event-driven runtime that makes it easy for developers to build resilient, stateless and stateful microservices that run on the cloud and edge and embraces the diversity of languages and developer frameworks.

    Dapr codifies the best practices for building microservice applications into open, independent, building blocks that enable you to build portable applications with the language and framework of your choice. Each building block is independent and you can use one, some, or all of them in your application.

    比上面的介绍多了 stateless or stateful 的标签。学《计算理论》的时候接触过一些状态机。

    ”状态是万恶之源“

    注意提到了多语言和多开发者框架,我认为这是他选择的通过通信共享信息,即 HTTPGRPC 支持多语言等特性。微软想通过这个设定一个构建微服务应用的规则。从根本上确立你开发的每一个应用的独立性。

    下面进行一个 QuickStart

    环境

    1. Install Docker(微服务已经离不开容器化了)
    2. Install Dapr
    3. Node.js version 8 or greater(这个 Helloworld 是 node 应用)

    On macOS

    Install the latest darwin Dapr CLI to /usr/local/bin

    1curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash

    有条件可以加速

    执行初始化(会启动 docker 容器)

    1$ dapr init
    2⌛  Making the jump to hyperspace...
    3Downloading binaries and setting up components
    4✅  Success! Dapr is up and running
    5
    6$ docker ps
    7CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                        NAMES
    8b3a5600e672f        redis                "docker-entrypoint.s…"   44 hours ago        Up 44 hours         0.0.0.0:6379->6379/tcp       xenodochial_hofstadter
    9e5010ba0c33f        daprio/dapr          "./placement"            44 hours ago        Up 44 hours         0.0.0.0:50005->50005/tcp     dapr_placement

    HelloWorld

    Application Architecture

    能够看到暴露两个 endpoint 是 HTTP 访问,一个创建一个查询。

    主要看我们使用 Dapr 的交互。在图中它作为 Runtime

    • 提供 Dapr API 给多语言调用。
    • 提供 状态管理 By state stores

    Download Code

    下载并进入相应文件夹

    1git clone https://github.com/dapr/samples.git
    2cd samples/1.hello-world

    Cat app.js

     1// $ cat app.js 
     2// ------------------------------------------------------------
     3// Copyright (c) Microsoft Corporation.
     4// Licensed under the MIT License.
     5// ------------------------------------------------------------
     6
     7const express = require('express');
     8const bodyParser = require('body-parser');
     9require('isomorphic-fetch');
    10
    11const app = express();
    12app.use(bodyParser.json());
    13
    14const daprPort = process.env.DAPR_HTTP_PORT || 3500;
    15const stateUrl = `http://localhost:${daprPort}/v1.0/state`;
    16const port = 3000;
    17
    18app.get('/order', (_req, res) => {
    19    fetch(`${stateUrl}/order`)
    20        .then((response) => {
    21            return response.json();
    22        }).then((orders) => {
    23            res.send(orders);
    24        });
    25});
    26
    27app.post('/neworder', (req, res) => {
    28    const data = req.body.data;
    29    const orderId = data.orderId;
    30    console.log("Got a new order! Order ID: " + orderId);
    31
    32    const state = [{
    33        key: "order",
    34        value: data
    35    }];
    36
    37    fetch(stateUrl, {
    38        method: "POST",
    39        body: JSON.stringify(state),
    40        headers: {
    41            "Content-Type": "application/json"
    42        }
    43    }).then((response) => {
    44        console.log((response.ok) ? "Successfully persisted state" : "Failed to persist state");
    45    });
    46
    47    res.status(200).send();
    48});
    49
    50app.listen(port, () => console.log(`Node App listening on port ${port}!`));

    这是一些路由和 handlers

    注意 14-16 行

    1const daprPort = process.env.DAPR_HTTP_PORT || 3500;
    2const stateUrl = `http://localhost:${daprPort}/v1.0/state`;
    3const port = 3000;

    3500 是 Dapr 的环境端口,如果你安装时有改动,需要考虑。

    stateurl 就是 Dapr 提供的 URL 了

    Handlers

    /neworder
     1app.post('/neworder', (req, res) => {
     2    const data = req.body.data;
     3    const orderId = data.orderId;
     4    console.log("Got a new order! Order ID: " + orderId);
     5
     6    const state = [{
     7        key: "order",
     8        value: data
     9    }];
    10
    11    fetch(stateUrl, {
    12        method: "POST",
    13        body: JSON.stringify(state),
    14        headers: {
    15            "Content-Type": "application/json"
    16        }
    17    }).then((response) => {
    18        console.log((response.ok) ? "Successfully persisted state" : "Failed to persist state");
    19    });
    20
    21    res.status(200).send();
    22});

    这里重点是状态存储,即将 state 通过 stateurl 存储在 Dapr 中。

    /order

    我们并不是直接通过 res.json 作为 Response 来进行已经持久化的数据的使用,而是通过暴露一个 GET endpoint 通过访问它来验证持久化是否成功。

    1app.get('/order', (_req, res) => {
    2    fetch(`${stateUrl}/order`)
    3        .then((response) => {
    4            return response.json();
    5        }).then((orders) => {
    6            res.send(orders);
    7        });
    8});

    现在我们通过状态转移在 Dapr 里实现了 stateless,同样我们也可以在加上一个 local cache 并通过一个新的 endpoint 访问来使 Node application 变成 stateful

    Dapr Run Node.js App

    1. npm install :通过当前目录下的 package.json , 会安装 express 和 body-parser ,在 app.js 7-8行我们可以看到这两项。
    2. dapr run --app-id mynode --app-port 3000 --port 3500 node app.js
    1$ dapr run --app-id mynode --app-port 3000 --port 3500 node app.js
    2ℹ️  Starting Dapr with id mynode. HTTP Port: 3500. gRPC Port: 55099
    3✅  You're up and running! Both Dapr and your app logs will appear here.

    应该是有后台运行的 CLI 命令,这里是前台打印的日志

     1== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="starting Dapr Runtime -- version 0.1.0 -- commit 4358565-dirty"
     2== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="log level set to: info"
     3== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="standalone mode configured"
     4== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="dapr id: mynode"
     5== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="loaded component messagebus (pubsub.redis)"
     6== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="loaded component statestore (state.redis)"
     7== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="application protocol: http. waiting on port 3000"
     8== APP == Node App listening on port 3000!
     9== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="application discovered on port 3000"
    10== DAPR == 2019/11/06 10:37:42 redis: connecting to localhost:6379
    11== DAPR == 2019/11/06 10:37:42 redis: connected to localhost:6379 (localAddr: [::1]:55130, remAddr: [::1]:6379)
    12== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actor runtime started. actor idle timeout: 1h0m0s. actor scan interval: 30s"
    13== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: starting connection attempt to placement service at localhost:50005"
    14== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="http server is running on port 3500"
    15== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="gRPC server is running on port 55099"
    16== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="local service entry announced"
    17== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="dapr initialized. Status: Running. Init Elapsed 945.8297490000001ms"
    18== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: established connection to placement service at localhost:50005"
    19== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: placement order received: lock"
    20== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: placement order received: update"
    21== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: placement tables updated"
    22== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: placement order received: unlock"

    ⚠️:注意到 Node App 在指定的 3000 端口运行,同时还有状态存储的 redis6379 端口运行

    Post and Get

    接下来注意,文中的端口是 app.js 里默认的 3500

    Post

    Curl
    1curl -XPOST -d @sample.json http://localhost:3500/v1.0/invoke/mynode/method/neworder
    vscode

    如果你用 vscode ,使用这个插件 Rest Client Plugin

    然后打开目录下的 sample.http , 可以看到 send request 的选项

    sample.http

    1POST http://localhost:3500/v1.0/invoke/mynode/method/neworder
    2{
    3  "data": {
    4    "orderId": "42"
    5  } 
    6}
    Postman

    如图: http://localhost:3500/v1.0/invoke/mynode/method/neworder 

    Result Update

    你可以在你启动的终端中看到新的日志

    1== APP == Got a new order! Order ID: 42
    2== APP == Successfully persisted state

    Get

    Curl
    1curl http://localhost:3500/v1.0/invoke/mynode/method/order
    Vscode

    sample.http

    1GET http://localhost:3500/v1.0/invoke/mynode/method/order
    Postman

    image.png

    Terminate

    ctrl + c 或者 dapr stop --app-id mynode

    1^C
    2ℹ️  terminated signal received: shutting down
    3✅  Exited Dapr successfully
    4✅  Exited App successfully

    Feature

    • 具有可插入提供程序和至少一次语义的事件驱动的Pub-Sub系统
    • 使用可插入提供程序的输入和输出绑定
    • 具有可插拔数据存储的状态管理
    • 一致的服务到服务发现和调用
    • 选择加入状态模型:强大/最终一致性,首次写入/最后写入获胜
    • 跨平台虚拟演员
    • 限速
    • 使用OpenTelemetry的内置分布式跟踪
    • 使用专用的Operator和CRD在Kubernetes上本地运行
    • 通过HTTP和gRPC支持所有编程语言
    • 来自Azure,AWS,GCP的多云,开放式组件(绑定,发布-订阅,状态)
    • 作为过程或容器化在任何地方运行
    • 轻量级(58MB二进制,4MB物理内存)
    • 作为辅助工具运行-无需特殊的SDK或库
    • 专用的CLI-易于调试的开发人员友好体验
    • Java,Dotnet,Go,Javascript和Python的客户端

    Refer

    来源: Dapr HelloWorld · 语雀

  • 还在篮子里

    icework + gh-pages 超快部署超多模版页面 · 语雀

    icework + gh-pages 超快部署超多模版页面

    项目地址:https://github.com/yhyddr/landingpage

    效果地址:https://yhyddr.github.io/landingpage


    前言

    • GitHub 账号 与它的 pages 服务

    不需要任何准备的东西,服务器?域名?前端工程师? 都不需要!只需要你有

    就能够享受到建立自己网站的乐趣。

    • 飞冰

    现在搭配飞冰,还能让你一键生成你喜欢的页面:最最主要的是,你的这个项目是一个 React App,你可以边学前端边改进你的 网站。实时热更新查看自己的编辑成果。一键部署到网站供大家访问,你值得拥有。

    关于飞冰

    简单而友好的前端研发体系

    特性

    • 可视化开发:通过 GUI 操作简化前端工程复杂度,同时通过适配器可接入不同的项目工程进行可视化管理,定制专有的前端工作台
    • 丰富的物料:基于物料拼装提高项目开发效率,同时提供丰富的 React/Vue 物料
    • 最佳实践:结合丰富的经验沉淀出的项目开发最佳实践,包括目录结果、开发调试、路由配置、状态管理等
    • 自定义物料:通过物料开发者工具快速开发构建私有物料体系

    另外,飞冰正确用法我觉得应该是企业搭建自己的物料库使用。

    关于 GitHub Pages

    Websites for you and your projects.

    Hosted directly from your GitHub repository. Just edit, push, and your changes are live.

    非常方便的网站托管,直接使用 你的 GitHub 项目库构建。

    安装飞冰

    首先安装飞冰

    1# 安装工具
    2$ npm install iceworks -g
    3# 启动工作台
    4$ iceworks

    创建项目

    这里我们选择基于推荐模板开始创建:

    • 选择你喜欢的一个模版,用于快速部署

    image.png

    • 新建一个文件夹或者选择已有的空文件夹(避免覆盖原有文件);
    • 给项目起一个项目名,以便后续识别。

    image.png

    Do something

    你可以随意看看控制台有哪些选项,或者修修改改。

    我们在这里主要看部署如何操作,所以直接跳过。

    部署

    Github

    创建一个新的 GitHub 的项目仓库。

    项目

    打开刚才创建的目录,找到 package.json 文件,并添加一下三项

    image.png

    homepage

    image.png

    这里填写你的账户和你的项目地址,如我的地址是 yhyddr/landingpage.

    你的应该填写  https://{{yourGithubName}}.github.io/{{yourProjectName}}

    predeploy & deploy

    将这两行加入到 scripts 中

    1"predeploy": "npm run build", 
    2"deploy": "gh-pages -d build"

    终端命令

    打开 项目所在文件的终端 执行以下操作

    推到远端仓库存储代码

    注意换成你自己的仓库名字

    1git init
    2git add README.md
    3git commit -m "first commit"
    4git remote add origin git@github.com:{yourName}/{yourProjectName}.git
    5git push -u origin master

    安装 gh-pages

    1npm install gh-pages --save-dev

    推送页面构建文件

    1npm run deploy

    之后如果本地有更改,就可以直接使用这条命令更新你的页面了。

    最后

    找到 GitHub 的 setting 页面

    image.png

    选择 使用 gh-pages 分支进行网站构建。

    image.png

    然后你的网站就可以在提示的网址进行访问了。

    自定义域名

    如果你拥有自己的域名,甚至可以直接设置在这里

    image.png

    不过不要忘记在自己的域名服务商那里解析为 GitHub 的 IP 地址哦。

    image.png

    image.png

    效果总结

    轻松拥有了一个自己的网站

    image.png

    同时只需要打开编辑器就可以非常快速的自定义化。

    还有诸多组件任意选择帮助构建你自己的网站。

    还在等什么!像一个前端工程师一样构建网站吧。

    还一键部署哦。

    参考

    https://monsoir.github.io/Notes/React/react/react-github-pages.html

    https://ice.work/docs/guide/about

    https://pages.github.com/

    https://ice.work/docs/guide/start

    来源: icework + gh-pages 超快部署超多模版页面 · 语雀

  • 还在篮子里

    bytes包 · 语雀

    bytes包

    Overview buffer.go

    这是 bytes 包里的 buffer 实现

    一图胜千言

    看不懂图的再看下面吧

    buffer.jpg

    核心函数

    Buffer 结构

    这是 buffer 的内部结构

    buf 字节切片,用来存储 buffer 的内容

    off 是代表从哪里开始读

    bootstrap 用来作为字节切片过小的时候防止多次申请空间减小开销

    lastRead 用来记录上一次的操作

     1// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
     2// The zero value for Buffer is an empty buffer ready to use.
     3// 注意 buffer 的零值是空的 buf
     4type Buffer struct {
     5    buf       []byte   // contents are the bytes buf[off : len(buf)]
     6    off       int      // read at &buf[off], write at &buf[len(buf)]
     7    bootstrap [64]byte // memory to hold first slice; helps small buffers avoid allocation.
     8    lastRead  readOp   // last read operation, so that Unread* can work correctly.
     9
    10    // FIXME: it would be advisable to align Buffer to cachelines to avoid false
    11    // sharing.
    12}
    

    Grow(n int)

    申请扩展缓冲区

     1// Grow grows the buffer's capacity, if necessary, to guarantee space for
     2// another n bytes. After Grow(n), at least n bytes can be written to the
     3// buffer without another allocation.
     4// If n is negative, Grow will panic.
     5// If the buffer can't grow it will panic with ErrTooLarge.
     6// 增加容量 n byte
     7func (b *Buffer) Grow(n int) {
     8    if n < 0 {
     9        panic("bytes.Buffer.Grow: negative count")
    10    }
    11    m := b.grow(n)
    12    b.buf = b.buf[:m]
    13}

    WriteString(s string) (n int, err error)

    向 buffer 中写字符串

     1// WriteString appends the contents of s to the buffer, growing the buffer as
     2// needed. The return value n is the length of s; err is always nil. If the
     3// buffer becomes too large, WriteString will panic with ErrTooLarge.
     4// 直接写 string 也行,同时自动扩展
     5func (b *Buffer) WriteString(s string) (n int, err error) {
     6    b.lastRead = opInvalid
     7    //先尝试不用扩展容量的写法
     8    m, ok := b.tryGrowByReslice(len(s))
     9    if !ok {
    10        m = b.grow(len(s))
    11    }
    12    // copy 可以直接把 string 类型作为 字节切片拷贝过去
    13    return copy(b.buf[m:], s), nil
    14}
    

    也有写字节切片的形式  Write(p []byte) (n int, err error)

    ReadFrom(r io.Reader) (n int64, err error)

    从 io.Reader 读取数据到 buffer 中

     1// ReadFrom reads data from r until EOF and appends it to the buffer, growing
     2// the buffer as needed. The return value n is the number of bytes read. Any
     3// error except io.EOF encountered during the read is also returned. If the
     4// buffer becomes too large, ReadFrom will panic with ErrTooLarge.
     5// 从实现了 io.Reader 接口的 r 中读取到 EOF 为止,如果超出了 maxInt 那么大就会返回太
     6// 大不能通过一个 [maxInt]byte 字节切片来存储了
     7func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
     8    b.lastRead = opInvalid
     9    for {
    10        i := b.grow(MinRead)
    11        // grow 申请了 n 个空间之后,会将 buffer 中的字节切片延长长度到 n 个字节之后
    12        // 所以需要重新赋值一下长度,避免一些误解,保证长度都是有效数据提供的
    13        b.buf = b.buf[:i]
    14        // 将 r 中的数据读到 buffer 中去
    15        m, e := r.Read(b.buf[i:cap(b.buf)])
    16        if m < 0 {
    17            panic(errNegativeRead)
    18        }
    19
    20        // 手动更改长度
    21        b.buf = b.buf[:i+m]
    22        n += int64(m)
    23        if e == io.EOF {
    24            return n, nil // e is EOF, so return nil explicitly
    25        }
    26        if e != nil {
    27            return n, e
    28        }
    29    }
    30}

    WriteTo(w io.Writer) (n int64, err error)

    向 io.Writer 中写数据

     1// WriteTo writes data to w until the buffer is drained or an error occurs.
     2// The return value n is the number of bytes written; it always fits into an
     3// int, but it is int64 to match the io.WriterTo interface. Any error
     4// encountered during the write is also returned.
     5func (b *Buffer) WriteTo(w io.Writer) (n int64, err error) {
     6    b.lastRead = opInvalid
     7    if nBytes := b.Len(); nBytes > 0 {
     8        //从 off 开始读的地方算起,全部写到 io.Writer 中去
     9        m, e := w.Write(b.buf[b.off:])
    10        //写的多了就报错
    11        if m > nBytes {
    12            panic("bytes.Buffer.WriteTo: invalid Write count")
    13        }
    14        //记录写过了多少,位移 offset 指针
    15        b.off += m
    16
    17        n = int64(m)
    18        if e != nil {
    19            return n, e
    20        }
    21        // all bytes should have been written, by definition of
    22        // Write method in io.Writer
    23        // 因为刚才判断过写多了的情况,所以这里是写少了
    24        if m != nBytes {
    25            return n, io.ErrShortWrite
    26        }
    27    }
    28    // Buffer is now empty; reset.
    29    // 写完之后重置
    30    b.Reset()
    31    return n, nil
    32}

    ReadBytes(delim byte) (line []byte, err error)

    用来读到终止符就结束,返回的是一个 line 字节切片包含终止符前的数据

     1// ReadBytes reads until the first occurrence of delim in the input,
     2// returning a slice containing the data up to and including the delimiter.
     3// If ReadBytes encounters an error before finding a delimiter,
     4// it returns the data read before the error and the error itself (often io.EOF).
     5// ReadBytes returns err != nil if and only if the returned data does not end in
     6// delim.
     7// 读取到终止符为止,就结束
     8func (b *Buffer) ReadBytes(delim byte) (line []byte, err error) {
     9    slice, err := b.readSlice(delim)
    10    // return a copy of slice. The buffer's backing array may
    11    // be overwritten by later calls.
    12    line = append(line, slice...)
    13    return line, err
    14}
    

    NewBuffer(buf []byte) *Buffer

    用来新建一个新的 Buffer ,其实也可以使用 new 和 var 来声明

     1// NewBuffer creates and initializes a new Buffer using buf as its
     2// initial contents. The new Buffer takes ownership of buf, and the
     3// caller should not use buf after this call. NewBuffer is intended to
     4// prepare a Buffer to read existing data. It can also be used to size
     5// the internal buffer for writing. To do that, buf should have the
     6// desired capacity but a length of zero.
     7//
     8// In most cases, new(Buffer) (or just declaring a Buffer variable) is
     9// sufficient to initialize a Buffer.
    10// 通过字节切片创建一个 buffer ,字节切片会保留初始值
    11// 在渴望容量但是长度为 0?的情况下
    12// 也可以当作内核的 buffer 来写入
    13func NewBuffer(buf []byte) *Buffer { return &Buffer{buf: buf} }

    同时也有通过 string 类型的实现

    func NewBufferString(s string) *Buffer {return &Buffer{buf: []byte(s)}}

    总结

    缓冲区,实现了大小控制,字节切片和 string 类型的读写,同时还对情况进行了优化,比如存在 bootstrap,比如 grow 函数中的多次检定。适合多读精读来学习

    Overview reader.go

    这个太简单,没什么核心的东西,就是实现了reader的接口实例

    结构

     1// A Reader implements the io.Reader, io.ReaderAt, io.WriterTo, io.Seeker,
     2// io.ByteScanner, and io.RuneScanner interfaces by reading from
     3// a byte slice.
     4// Unlike a Buffer, a Reader is read-only and supports seeking.
     5// 实现了读取的各种方法,与 buffer 不同的是,只读同时支持位置
     6type Reader struct {
     7    s        []byte
     8    i        int64 // current reading index
     9    prevRune int   // index of previous rune; or < 0
    10}

    总结

    注意该 bytes.Reader 是只读的。

    Overview bytes.go

    操作字节切片的函数,与字符串 strings  包类似。

    核心函数

    genSplit(s, sep []byte, sepSave, n int) [][]byte

    切分切片使用的最核心的函数。

    有四个参数,第一个是被切切片,第二个是分隔符,第三个是选择包含分隔符在内往后几个字节一起作为子切片,最后一个是最多通过n个分隔符来切分

     1// Generic split: splits after each instance of sep,
     2// including sepSave bytes of sep in the subslices.
     3// 将含有 sep 的字节切片全部单独切开,最多切 n 个,同时 匹配到时候多切 sepSave 个字节一起切进同一个切片
     4func genSplit(s, sep []byte, sepSave, n int) [][]byte {
     5    if n == 0 {
     6        return nil
     7    }
     8    if len(sep) == 0 {
     9        return explode(s, n)
    10    }
    11    if n < 0 {
    12        n = Count(s, sep) + 1
    13    }
    14
    15    a := make([][]byte, n)
    16    n--
    17    i := 0
    18    for i < n {
    19        m := Index(s, sep)
    20        if m < 0 {
    21            break
    22        }
    23        a[i] = s[: m+sepSave : m+sepSave]
    24        s = s[m+len(sep):]
    25        i++
    26    }
    27    a[i] = s
    28    return a[:i+1]
    29}
    

    Fields(s []byte) [][]byte

    主要是可以消除多个分隔符连续的噪声

    这里的巧妙的地方时通过了一个 uint8 数组来实现了 ASCII 编码的空格的判定,还是使用位来判定是否存在非ASCII编码加快分隔速度。

    有一个 FieldsFunc 函数来自定义规则

     1var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
     2
     3// Fields interprets s as a sequence of UTF-8-encoded code points.
     4// It splits the slice s around each instance of one or more consecutive white space
     5// characters, as defined by unicode.IsSpace, returning a slice of subslices of s or an
     6// empty slice if s contains only white space.
     7func Fields(s []byte) [][]byte {
     8    // First count the fields.
     9    // This is an exact count if s is ASCII, otherwise it is an approximation.
    10    n := 0
    11    wasSpace := 1
    12    // setBits is used to track which bits are set in the bytes of s.
    13  // 意思就是通过位来判断是否所有的都可以通过字节来表示而不是需要utf-8编码
    14    setBits := uint8(0)
    15  // 这里实现了如果连续出现空格不会多次计数的除噪,通过 wasSpace
    16    for i := 0; i < len(s); i++ {
    17        r := s[i]
    18        setBits |= r
    19        isSpace := int(asciiSpace[r])
    20        n += wasSpace & ^isSpace
    21        wasSpace = isSpace
    22    }
    23    //不能通过ASCII码了就用utf-8
    24    if setBits >= utf8.RuneSelf {
    25        // Some runes in the input slice are not ASCII.
    26        return FieldsFunc(s, unicode.IsSpace)
    27    }
    28
    29    // ASCII fast path 更快
    30    a := make([][]byte, n)
    31    na := 0
    32    fieldStart := 0
    33    i := 0
    34    // Skip spaces in the front of the input.
    35  // 跳过开头的空格
    36    for i < len(s) && asciiSpace[s[i]] != 0 {
    37        i++
    38    }
    39    fieldStart = i
    40    for i < len(s) {
    41        if asciiSpace[s[i]] == 0 {
    42            i++
    43            continue
    44        }
    45        a[na] = s[fieldStart:i:i]
    46        na++
    47        i++
    48        // Skip spaces in between fields.
    49        for i < len(s) && asciiSpace[s[i]] != 0 {
    50            i++
    51        }
    52        fieldStart = i
    53    }
    54  // 弥补上面的判断可能最后的EOF会忽略
    55    if fieldStart < len(s) { // Last field might end at EOF.
    56        a[na] = s[fieldStart:len(s):len(s)]
    57    }
    58    return a
    59}
    

    Join(s [][]byte, sep []byte) []byte

    有分离就有连结,通过 sep 分隔符插在中间。

     1// Join concatenates the elements of s to create a new byte slice. The separator
     2// sep is placed between elements in the resulting slice.
     3func Join(s [][]byte, sep []byte) []byte {
     4    if len(s) == 0 {
     5        return []byte{}
     6    }
     7    if len(s) == 1 {
     8        // Just return a copy.
     9        return append([]byte(nil), s[0]...)
    10    }
    11  //判断需要多长的切片
    12    n := len(sep) * (len(s) - 1)
    13    for _, v := range s {
    14        n += len(v)
    15    }
    16
    17    b := make([]byte, n)
    18    bp := copy(b, s[0])
    19    for _, v := range s[1:] {
    20        bp += copy(b[bp:], sep)
    21        bp += copy(b[bp:], v)
    22    }
    23    return b
    24}

    Map(mapping func(r rune) rune, s []byte) []byte

    通过映射函数替换切片中满足条件的字节

     1// Map returns a copy of the byte slice s with all its characters modified
     2// according to the mapping function. If mapping returns a negative value, the character is
     3// dropped from the byte slice with no replacement. The characters in s and the
     4// output are interpreted as UTF-8-encoded code points.
     5
     6func Map(mapping func(r rune) rune, s []byte) []byte {
     7    // In the worst case, the slice can grow when mapped, making
     8    // things unpleasant. But it's so rare we barge in assuming it's
     9    // fine. It could also shrink but that falls out naturally.
    10    maxbytes := len(s) // length of b
    11    nbytes := 0        // number of bytes encoded in b
    12    b := make([]byte, maxbytes)
    13    for i := 0; i < len(s); {
    14        wid := 1
    15        r := rune(s[i])
    16        if r >= utf8.RuneSelf {
    17            r, wid = utf8.DecodeRune(s[i:])
    18        }
    19        r = mapping(r)
    20        if r >= 0 {
    21            rl := utf8.RuneLen(r)
    22            if rl < 0 {
    23                rl = len(string(utf8.RuneError))
    24            }
    25            if nbytes+rl > maxbytes {
    26                // Grow the buffer.
    27                maxbytes = maxbytes*2 + utf8.UTFMax
    28                nb := make([]byte, maxbytes)
    29                copy(nb, b[0:nbytes])
    30                b = nb
    31            }
    32            nbytes += utf8.EncodeRune(b[nbytes:maxbytes], r)
    33        }
    34        i += wid
    35    }
    36    return b[0:nbytes]
    37}

    indexFunc(s []byte, f func(r rune) bool, truth bool) int

    返回满足条件函数的 rune 的下标,未找到就返回-1

    条件函数可以是满足条件,可以是不满足条件,看变量 truth 的使用

     1// indexFunc is the same as IndexFunc except that if
     2// truth==false, the sense of the predicate function is
     3// inverted.
     4func indexFunc(s []byte, f func(r rune) bool, truth bool) int {
     5    start := 0
     6    for start < len(s) {
     7        wid := 1
     8        r := rune(s[start])
     9    //如果是utf-8编码才能识别,则调用utf-8.DecodeRune(s[start:])
    10        if r >= utf8.RuneSelf {
    11            r, wid = utf8.DecodeRune(s[start:])
    12        }
    13        if f(r) == truth {
    14            return start
    15        }
    16        start += wid
    17    }
    18    return -1
    19}

    makeCutsetFunc(cutset string) func(r rune) bool

    通过传入 的 string 类型变量,作为判断的条件函数,该函数判断 如果是 string 蕴含的返回真否则假

     1func makeCutsetFunc(cutset string) func(r rune) bool {
     2    if len(cutset) == 1 && cutset[0] < utf8.RuneSelf {
     3        return func(r rune) bool {
     4            return r == rune(cutset[0])
     5        }
     6    }
     7    if as, isASCII := makeASCIISet(cutset); isASCII {
     8        return func(r rune) bool {
     9            return r < utf8.RuneSelf && as.contains(byte(r))
    10        }
    11    }
    12    return func(r rune) bool {
    13        for _, c := range cutset {
    14            if c == r {
    15                return true
    16            }
    17        }
    18        return false
    19    }
    20}
    

    帮助实现的使用次数较多的函数

    DecodeRune(p []byte) (r rune, size int)

     1// DecodeRune unpacks the first UTF-8 encoding in p and returns the rune and
     2// its width in bytes. If p is empty it returns (RuneError, 0). Otherwise, if
     3// the encoding is invalid, it returns (RuneError, 1). Both are impossible
     4// results for correct, non-empty UTF-8.
     5//
     6// An encoding is invalid if it is incorrect UTF-8, encodes a rune that is
     7// out of range, or is not the shortest possible UTF-8 encoding for the
     8// value. No other validation is performed.
     9func DecodeRune(p []byte) (r rune, size int) {
    10    n := len(p)
    11    if n < 1 {
    12        return RuneError, 0
    13    }
    14    p0 := p[0]
    15    x := first[p0]
    16    if x >= as {
    17        // The following code simulates an additional check for x == xx and
    18        // handling the ASCII and invalid cases accordingly. This mask-and-or
    19        // approach prevents an additional branch.
    20        mask := rune(x) << 31 >> 31 // Create 0x0000 or 0xFFFF.
    21        return rune(p[0])&^mask | RuneError&mask, 1
    22    }
    23    sz := x & 7
    24    accept := acceptRanges[x>>4]
    25    if n < int(sz) {
    26        return RuneError, 1
    27    }
    28    b1 := p[1]
    29    if b1 < accept.lo || accept.hi < b1 {
    30        return RuneError, 1
    31    }
    32    if sz == 2 {
    33        return rune(p0&mask2)<<6 | rune(b1&maskx), 2
    34    }
    35    b2 := p[2]
    36    if b2 < locb || hicb < b2 {
    37        return RuneError, 1
    38    }
    39    if sz == 3 {
    40        return rune(p0&mask3)<<12 | rune(b1&maskx)<<6 | rune(b2&maskx), 3
    41    }
    42    b3 := p[3]
    43    if b3 < locb || hicb < b3 {
    44        return RuneError, 1
    45    }
    46    return rune(p0&mask4)<<18 | rune(b1&maskx)<<12 | rune(b2&maskx)<<6 | rune(b3&maskx), 4
    47}

    Equal(a, b []byte) bool

    1//go:noescape
    2
    3// Equal returns a boolean reporting whether a and b
    4// are the same length and contain the same bytes.
    5// A nil argument is equivalent to an empty slice.
    6func Equal(a, b []byte) bool // in internal/bytealg
    

    总结

    实现了几乎所有能对字节切片产生的操作,基本都是基于 utf-8 编码来判定的,或者使用 ASCII 码当可以使用的时候,实现了

    • 分隔    各种规则的分隔符分隔(包括自定义规则)
    • 裁剪    内置左右匹配的裁剪(自定义规则)和裁剪空格符
    • 粘合
    • 索引
    • 替换
      • 各种规则的替换
      • 内置大小写和标题字体的替换
    • 这些都是在包内分成了小函数来实现增强可自定义的性质,比如内置实现一些判断是否有前缀,是否包含某些编码,就像造好了手枪和一些子弹,想要更多功能直接制造特制子弹即可。如果关心这个功能模块化请看带 Func 签名的函数即可

    Ps:包含有 Rabin-Karp search 的实现,被使用在 Index 这个返回索引的函数中。

    来源: bytes包 · 语雀

  • 还在篮子里

    beehive 源码详解 · 语雀

    beehive 源码详解

    Overview

    看一下作者本人的注释

    // Package bees is Beehive's central module system.

    beehive 非常有趣的在于各逻辑的解耦设计,这不仅让本身功能操作简单,也让扩展变得关注点少了很多,只需要一点学习成本就可以扩展自己的 beehive

    首先解释一下 bee hive 中 的概念

    bee 代表的是我们常见的 Worker 也就是说,实际的行为是由这些 小蜜蜂执行的。他们就类似于采蜜的工人,采集到了之后统一放回来处理

    hive 是蜂房,也就是我们常见的 WorkerPool 不同的是,她更像一个 Facotry ,什么意思呢?她可以创建专属的 bee 。在极少的配置下,比如只需要配置上一个 token 即可。就可以生成一只 bee 专门针对某一种蜜工作了。

    chain 又是什么? chain 就是链接事件与处理的工具,我们认为 bee 采回蜜是一个事件,总不可能采回来啥都不干吧。针对不同的 蜜 我们就有不同的反应,就有不同的 action

    比如某人的 blog 更新了 ,rss bee 接收到了之后飞回来,我们就可以再要求 email bee 把这其中的信息通过邮件发给我们或者我们想发给的人。

    这就需要 chain 来联系 event 和 action 了

    Landscape

    API Register

    beehive-api (1).svg

    成组的 API 实现了 Resource 接口注册到 Container 的路由 Route 中。

    帮助理解

    在文件中写好了 作者 实现的 Handle 的实现来注册 http 请求

    image.png

    API 只是供调用,逻辑重点在 bees 这个包里的实现。

    Bee

    首先是有的接口

     1
     2// BeeInterface is an interface all bees implement.
     3type BeeInterface interface {
     4    // Name of the bee
     5    Name() string
     6    // Namespace of the bee
     7    Namespace() string
     8
     9    // Description of the bee
    10    Description() string
    11    // SetDescription sets a description
    12    SetDescription(s string)
    13
    14    // Config returns this bees config
    15    Config() BeeConfig
    16    // Options of the bee
    17    Options() BeeOptions
    18    // SetOptions to configure the bee
    19    SetOptions(options BeeOptions)
    20
    21    // ReloadOptions gets called after a bee's options get updated
    22    ReloadOptions(options BeeOptions)
    23
    24    // Activates the bee
    25    Run(eventChannel chan Event)
    26    // Running returns the current state of the bee
    27    IsRunning() bool
    28    // Start the bee
    29    Start()
    30    // Stop the bee
    31    Stop()
    32
    33    LastEvent() time.Time
    34    LogEvent()
    35    LastAction() time.Time
    36    LogAction()
    37
    38    Logln(args ...interface{})
    39    Logf(format string, args ...interface{})
    40    LogErrorf(format string, args ...interface{})
    41    LogFatal(args ...interface{})
    42
    43    SetSigChan(c chan bool)
    44    WaitGroup() *sync.WaitGroup
    45
    46    // Handles an action
    47    Action(action Action) []Placeholder
    48}
    

    和他的基础实现

     1// Bee is the base-struct to be embedded by bee implementations.
     2type Bee struct {
     3    config BeeConfig
     4
     5    lastEvent  time.Time
     6    lastAction time.Time
     7
     8    Running   bool
     9    SigChan   chan bool
    10    waitGroup *sync.WaitGroup
    11}

    这里需要注意的是 Run 接口,在 Base-Struct Bee 中该方法 是空的实现,因为 Run 是 Bee 的生命周期开始处,是自动开始的。

    WebBee 实现

    简单的看某一个实现即可

    1// WebBee is a Bee that starts an HTTP server and fires events for incoming
    2// requests.
    3type WebBee struct {
    4    bees.Bee
    5
    6    addr string
    7
    8    eventChan chan bees.Event
    9}

    可以很清楚的指导,这个 WebBee 中的 eventChan 正是通知的地方,也就是上文所说的 Chain 的开始处。注意的是由于松耦合的设计,任何 Bee 都可以成为 Chain 上的一环,只要它能触发事件。或者监听事件。

    func (mod *WebBee) Run(cin chan bees.Event)
     1
     2// Run executes the Bee's event loop.
     3func (mod *WebBee) Run(cin chan bees.Event) {
     4    mod.eventChan = cin
     5
     6    srv := &http.Server{Addr: mod.addr, Handler: mod}
     7    l, err := net.Listen("tcp", mod.addr)
     8    if err != nil {
     9        mod.LogErrorf("Can't listen on %s", mod.addr)
    10        return
    11    }
    12    defer l.Close()
    13
    14    go func() {
    15        err := srv.Serve(l)
    16        if err != nil {
    17            mod.LogErrorf("Server error: %v", err)
    18        }
    19        // Go 1.8+: srv.Close()
    20    }()
    21
    22    select {
    23    case <-mod.SigChan:
    24        return
    25    }
    26}

    同时 WebBee 也有一个方法 ServeHTTP 来实现 http.Handle 来处理请求。

    这里也就是前文所说的 注册的那些 API 的部分来源,每一个 bee 自身实现的自动注册暴露给外界调用。

    1func (mod *WebBee) ServeHTTP(w http.ResponseWriter, req *http.Request)

    Event

    package:beehive/bees/event.go

    刚才讲到了 触发事件 event 的 WebBee 实现,现在我们来看 event 的实现

    实际上是通过 这个函数实现的

     1// handleEvents handles incoming events and executes matching Chains.
     2func handleEvents() {
     3    for {
     4        event, ok := <-eventsIn
     5        ···
     6        bee := GetBee(event.Bee)
     7        (*bee).LogEvent()
     8
     9        ···
    10        go func() {
    11            defer func() {
    12                if e := recover(); e != nil {
    13                    log.Printf("Fatal chain event: %s %s", e, debug.Stack())
    14                }
    15            }()
    16
    17            execChains(&event)
    18        }()
    19    }
    20}

    省略了 日志部分。可以看到 handleEvents 通过接受通道里的 event,并检查 event 中的 Bee 作为 标志找到对应的 Bee 唤醒。

    这里我们可以看到 最后进入了 Chains 中执行,即上文所说的 Chain 将 Event 和 Action 链接了起来,让 Bee 之间能够协作。

    chain

    package:beehive/bees/chains.go

    chain 中实际上是调用 Actions 通过下面的 execActions 函数

    1    for _, el := range c.Actions {
    2            action := GetAction(el)
    3            if action == nil {
    4                log.Println("\t\tERROR: Unknown action referenced!")
    5                continue
    6            }
    7            execAction(*action, m)
    8        }

    我们来看看 Action 的执行。

    Action Exec

    package: beehive/bees/actions.go

    actions 既可以运行设置中的 options 也可以直接在 运行函数中传入需要运行的 options

    func execAction(action Action, opts map[string]interface{}) bool

    bee-actions (1).svg

    Summary

    整个执行逻辑是如此了,其他还有一些

    • 日志处理:用于跟踪 bee 和 hive 的情况
    • Config:保存配置文件,每一次启动可以重新放飞以前的 Bee 们
    • Signal:beehive 拦截了一些 Signal 的 Kill 等信号来执行优雅退出,避免了 Config 等的丢失。
    • Run Flag:执行的附带参数,设定 Beehive 整个应用的监听端口和版本配置。
  • 还在篮子里

    rpc设计 · 语雀

    rpc设计

    框架

    Codec

    负责处理编解码,处理不同的网络传输协议或者文本

    • Marshall:2[]byte
    • UnMarshall:2protocol

    Server

    监听&调用,同时可以作为注册中心。

    • Register:注册服务到 Server 中
    • ServeConn:服务端口链接,处理并进行调用
      • ReadRequest:use codec read request
      • SendResponse: use codec write reply
    • Listen:监听不同协议请求

    Client

    客户端发起调用,接收返回数据

    • Call:远端调用
    • Go:异步远端调用

    Context&EndPoint

    中间件等,超时 TimeOut ,认证 Auth ,心跳 等实现

    使用

    Service&Call

    服务实例

    来源: rpc设计 · 语雀

  • 还在篮子里

    grpc 源码结构详解 · 语雀

    grpc 源码结构详解

    DialOptions

    DialOptions 是最重要的一环,负责配置每一次 rpc 请求的时候的一应选择。

    结构

    先来看看这个的结构

    链接

     1// dialOptions configure a Dial call. dialOptions are set by the DialOption
     2// values passed to Dial.
     3type dialOptions struct {
     4    unaryInt  UnaryClientInterceptor
     5    streamInt StreamClientInterceptor
     6
     7    chainUnaryInts  []UnaryClientInterceptor
     8    chainStreamInts []StreamClientInterceptor
     9
    10    cp          Compressor
    11    dc          Decompressor
    12    bs          backoff.Strategy
    13    block       bool
    14    insecure    bool
    15    timeout     time.Duration
    16    scChan      <-chan ServiceConfig
    17    authority   string
    18    copts       transport.ConnectOptions
    19    callOptions []CallOption
    20    // This is used by v1 balancer dial option WithBalancer to support v1
    21    // balancer, and also by WithBalancerName dial option.
    22    balancerBuilder balancer.Builder
    23    // This is to support grpclb.
    24    resolverBuilder             resolver.Builder
    25    channelzParentID            int64
    26    disableServiceConfig        bool
    27    disableRetry                bool
    28    disableHealthCheck          bool
    29    healthCheckFunc             internal.HealthChecker
    30    minConnectTimeout           func() time.Duration
    31    defaultServiceConfig        *ServiceConfig // defaultServiceConfig is parsed from defaultServiceConfigRawJSON.
    32    defaultServiceConfigRawJSON *string
    33}

    由于命名非常规范,加上注释很容易看懂每一个 field 配置的哪一条属性。如果掠过看的 大概有 压缩解压器,超时阻塞设置,认证安全转发,负载均衡,服务持久化的信息存储 ,配置,心跳检测等。

    其一应函数方法都是设置 其中字段的。

    如何设置

    这里是 grpc 设计较好的地方,通过函数设置,同时设有生成函数的函数。什么意思呢?首先结合图来理解,这也是整个 grpc 设置的精华部分

    grpc-setOperation.svg

    这里的意思是 , DialOptions 是一个导出接口,实现函数是 apply 同时接受参数 dialOptions 来修改它。

    而实际上,是使用 newFuncDialOption 函数包装一个 修改 dialOptions 的方法给 funcDialOption 结构体,在实际 Dial 调用的时候 是使用闭包 调用 funcDialOption 结构体的 apply 方法。

    可以在这里看一下 Dial 方法的源码(Dial 调用的是 DialContext

    起作用的就是 opt.apply()

     1func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
     2    cc := &ClientConn{
     3        target:            target,
     4        csMgr:             &connectivityStateManager{},
     5        conns:             make(map[*addrConn]struct{}),
     6        dopts:             defaultDialOptions(),
     7        blockingpicker:    newPickerWrapper(),
     8        czData:            new(channelzData),
     9        firstResolveEvent: grpcsync.NewEvent(),
    10    }
    11    ···
    12    for _, opt := range opts {
    13        opt.apply(&cc.dopts)
    14    }
    15    ···
    16}

    这里的 options 可以说是 client 发起 rpc 请求的核心中转站。

    另一个重要的接口,同时也集中在 dialOptions 结构体中初始化处理的是

    callOptions []CallOption

    CallOption

    CallOption 是一个接口,定义在 rpc_util 包内

    结构

     1// CallOption configures a Call before it starts or extracts information from
     2// a Call after it completes.
     3type CallOption interface {
     4    // before is called before the call is sent to any server.  If before
     5    // returns a non-nil error, the RPC fails with that error.
     6    before(*callInfo) error
     7
     8    // after is called after the call has completed.  after cannot return an
     9    // error, so any failures should be reported via output parameters.
    10    after(*callInfo)
    11}

    操作的是 callInfo 结构里的数据,其被包含在 dialOptions  结构体中,

    即每一次 dial 的时候进行调用。

    callInfo

    同时它自身定义很有意思,操作的是 callInfo  结构体

     1// callInfo contains all related configuration and information about an RPC.
     2type callInfo struct {
     3    compressorType        string
     4    failFast              bool
     5    stream                ClientStream
     6    maxReceiveMessageSize *int
     7    maxSendMessageSize    *int
     8    creds                 credentials.PerRPCCredentials
     9    contentSubtype        string
    10    codec                 baseCodec
    11    maxRetryRPCBufferSize int
    12}

    可以看到 callInfo 中字段用来表示 单次调用中独有的自定义选项如 压缩,流控,认证,编解码器等。

    一个实现

    简单看一个 CallOption 接口的实现

     1// Header returns a CallOptions that retrieves the header metadata
     2// for a unary RPC.
     3func Header(md *metadata.MD) CallOption {
     4    return HeaderCallOption{HeaderAddr: md}
     5}
     6
     7// HeaderCallOption is a CallOption for collecting response header metadata.
     8// The metadata field will be populated *after* the RPC completes.
     9// This is an EXPERIMENTAL API.
    10type HeaderCallOption struct {
    11    HeaderAddr *metadata.MD
    12}
    13
    14func (o HeaderCallOption) before(c *callInfo) error { return nil }
    15func (o HeaderCallOption) after(c *callInfo) {
    16    if c.stream != nil {
    17        *o.HeaderAddr, _ = c.stream.Header()
    18    }
    19}

    重点看到,实际操作是在 before 和 after 方法中执行,它们会在 Client 发起请求的时候自动执行,顾名思义,一个在调用前执行,一个在调用后执行。

    实现注意

    这里可以看出,这里也是通过函数返回一个拥有这两个方法的结构体,注意这一个设计,可以作为你自己的 Option 设计的时候的参考。

    两种方法

    有两种方法让 Client 接受你的 CallOption 设置

    1. 在 Client 使用方法的时候直接作为 参数传递,将刚才所说的函数-返回一个实现了 CallOption 接口的结构体。
    2. 在 生成 Client 的时候就传递设置。具体如下
      1. 通过 dialOptions.go 中的 函数 grpc.WithDefaultCallOptions()
      2. 这个函数会将 CallOption 设置到 dialOptions 中的字段 []CallOption 中。
    1// WithDefaultCallOptions returns a DialOption which sets the default
    2// CallOptions for calls over the connection.
    3func WithDefaultCallOptions(cos ...CallOption) DialOption {
    4    return newFuncDialOption(func(o *dialOptions) {
    5        o.callOptions = append(o.callOptions, cos...)
    6    })
    7}

    有没有感觉有点不好理解?给你们一个实例

    1. 使用的第一种方法
    1response, err := myclient.MyCall(ctx, request, grpc.CallContentSubtype("mycodec"))
    1. 使用第二种方法
    1myclient := grpc.Dial(ctx, target, grpc.WithDefaultCallOptions(grpc.CallContentSubtype("mycodec")))

    这里假设 我们设置了一个 mycodec 的译码器。马上下面解释它的设计。

    值得注意的是, 我好像只提到了在 Client 调用时设置,callOption  只在客户端设置的情况是不是让大家感到困惑。

    实际上 gRPC server 端会自动检测 callOption 的设置,并检测自己是否支持此项选择,如果不支持则会返回失败。也就是说,在 Server 端注册的所有 Codec 译码器之后,Client 直接使用相应的设置就好了。

    Codec

    在 gRPC 中 Codec 有两个接口定义,一个是 baseCodec 包含正常的 Marshal 和 Unmarshal 方法,另一个是拥有名字的 Codec 定义在 encoding 包内,这是由于在注册 registry 的时候会使用到这个方法。

    接口

    1type Codec interface {
    2    // Marshal returns the wire format of v.
    3    Marshal(v interface{}) ([]byte, error)
    4    // Unmarshal parses the wire format into v.
    5    Unmarshal(data []byte, v interface{}) error
    6    // String returns the name of the Codec implementation.  This is unused by
    7    // gRPC.
    8    String() string
    9}

    就是这个方法

     1// RegisterCodec registers the provided Codec for use with all gRPC clients and
     2// servers.
     3//
     4// The Codec will be stored and looked up by result of its Name() method, which
     5// should match the content-subtype of the encoding handled by the Codec.  This
     6// is case-insensitive, and is stored and looked up as lowercase.  If the
     7// result of calling Name() is an empty string, RegisterCodec will panic. See
     8// Content-Type on
     9// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for
    10// more details.
    11//
    12// NOTE: this function must only be called during initialization time (i.e. in
    13// an init() function), and is not thread-safe.  If multiple Compressors are
    14// registered with the same name, the one registered last will take effect.
    15func RegisterCodec(codec Codec) {
    16    if codec == nil {
    17        panic("cannot register a nil Codec")
    18    }
    19    if codec.Name() == "" {
    20        panic("cannot register Codec with empty string result for Name()")
    21    }
    22    contentSubtype := strings.ToLower(codec.Name())
    23    registeredCodecs[contentSubtype] = codec
    24}

    Compressor

    同时 encoding 包中还定义了 Compressor 接口,参照 Codec 理解即可。

     1// Compressor is used for compressing and decompressing when sending or
     2// receiving messages.
     3type Compressor interface {
     4    // Compress writes the data written to wc to w after compressing it.  If an
     5    // error occurs while initializing the compressor, that error is returned
     6    // instead.
     7    Compress(w io.Writer) (io.WriteCloser, error)
     8    // Decompress reads data from r, decompresses it, and provides the
     9    // uncompressed data via the returned io.Reader.  If an error occurs while
    10    // initializing the decompressor, that error is returned instead.
    11    Decompress(r io.Reader) (io.Reader, error)
    12    // Name is the name of the compression codec and is used to set the content
    13    // coding header.  The result must be static; the result cannot change
    14    // between calls.
    15    Name() string
    16}
    

    MetaData

    这个包对应 context 中的 Value field 也就是 key-value 形式的存储

    在其他包中简写是 MD

    结构

    1type MD map[string][]string

    函数

    实现了完善的存储功能,从单一读写到批量(采用 pair 模式,…string 作为参数,len(string)%2==1 时会报错,由于会有孤立的没有配对的元信息。

    另外几个函数是实现了从 context 中的读取和写入(这里的写入是 使用 context.WithValue 方法,即生成 parent context 的 copy。

    注意⚠️

    • 值得注意的是,在 MetaData 结构体中, value 的结构是 []string 。
    • 同时 key 不可以以 “grpc-” 开头,这是因为在 grpc 的 internal 包中已经保留了。
    • 更为重要的是 在 context 中的读取方式,其实是 MetaData 结构对应的是 context Value 中的 value 值,而 key 值设为 一个空结构体同时区分输入输入
      • type mdIncomingKey struct{}
      • type mdOutgoingKey struct{}

    Refer

    gRPC 官方也有 MetaData 的解释

    来源: grpc 源码结构详解 · 语雀

  • 还在篮子里

    rancher 部署 istio 实验 bookinfo · 语雀

    rancher 部署 istio 实验 bookinfo

    1. 打开 default 命名空间image.png
    2. 打开 应用商店, 点击启动或者 launch

    image.png

    1. 找到 istio 点开查看详情 image.png
    2. 默认配置即可,我们这个版本是 1.05image.png
    3. 可以选择开启 grafana,不过需要注意的是需要为集群 加 PV 即持久化存储

    image.png

    1. 点击启动即可
    2. 这样添加持久卷image.png
    3. 记得开启多主机读写image.png
    4. 一会儿可以看到部署完毕,如果中途遇到问题,通常是 网络问题image.png
    1. 有时需要手动为你的服务添加负载均衡image.png
    2. 注意选择命名空间,这里可以理解为添加路由。image.png
    3. 现在,你需要在Rancher的面板上检查Istio所有的工作负载、负载均衡以及服务发现均处于良好状态。即 active
    4. 最后还有一样东西需要添加:在你的默认命名空间里添加一个istio-injected标签,Istio sidecar容器会自动注入你的节点,运行下方的kubectl命令(如上文所述,你可以从Rancher内部启动kubectl)。image.png这一标签将使得Istio-Sidecar-Injector自动将Envoy容器注入您的应用程序节点。

    然后我们开始 bookinfo 的部署

    部署Bookinfo示例应用

    现在,你可以开始部署一个测试应用并且测试Istio的强大功能。首先,部署Bookinfo示例应用。这个应用有趣的部分在于它有三个版本的reviews程序同时运行。我们可-以在这三个版本的程序中体验到Istio的一些功能。接着,访问rancher-demo的默认项目中的工作负载来部署Bookinfo app,具体的操作是:

    点击Import Yaml;下载bookinfo.yaml(https://info.rancher.com/hubfs/bookinfo.yaml)到本地

    当你进入Import Yaml菜单之后,通过从文件读取,将其上传至Rancher;

    对于 Import Mode,选择【集群:将任何资源直接导入此集群】Cluster: Direct import of any resources into this cluster;

    点击【导入/Import】。

    这应该为您的rancher-demo Default项目增加6个工作负载。如下图:

    image.png

    现在,通过Istio暴露Bookinfo app,你需要应用此bookinfo-gateway.yaml(https://info.rancher.com/hubfs/bookinfo-gateway.yaml),操作方式与bookinfo.yaml相同。此时,你可以用浏览器访问bookinfo app。你有两种方式可以获取istio-ingressgateway负载均衡器的外部IP地址:

    第一,从Rancher中获取。访问负载均衡,从右手边的菜单栏选择View in API。它将打开一个新的浏览器页面,在那搜索publicEndpoints -> addresses,你就可以看到公共IP地址了。

    第二,通过kubectl获取:

    image.png

    用你的浏览器访问: http://${INGRESS_HOST}/productpage ,然后你应该看到Bookinfo app。多次刷新页面时,你应该看到 Book Reviews部分有三个不同版本:第一个版本没有星星;第二个版本有黑星星;第三个版本有红星星。

    使用Istio,您可以限制您的应用仅路由到应用的第一个版本。具体操作为:导入 route-rule-all-v1.yaml( https://info.rancher.com/hubfs/route-rule-all-v1.yaml )到Rancher,几秒之后再刷新页面,你将不会在reviews上看到任何星星。

    除此之外,你也可以仅将流量路由到一组用户。当你导入route-rule-reviews-test-v2.yaml到Rancher之后,使用jason这个用户名(无需密码)登录Bookinfo app,你应该只能看到版本2的reviews(即有黑星星的版本)。但登出之后,你仅能看到版本1reviews的app。

    至此,你已经体会了Istio的强大功能。当然,这并非全部,Istio还有很多其他功能。创建此设置后,您可以完成Istio文档中的任务。

    Istio的遥感

    现在是时候深入了解Istio另一个更有用的功能 :默认情况下提供指标。

    让我们从Grafana开始。当我们部署Istio时,值设置为true的grafana.enabled创建了一个grafana实例,并配置为收集Istio的指标以几个面板中显示它们。默认情况下,Grafana的服务不会公开显示,因此想要查看指标,首先需要将Grafana的服务暴露给公共IP地址。当然,还有另一个选项也可以暴露服务:NodePort(https://kubernetes.io/docs/concepts/services-networking/service/#nodeport ),但是这要求你在Google Cloud Platform防火墙的所有节点上开放Nodeport,这不止有一项任务,因此通过公共IP地址暴露服务更为简单。

    为此,在rancher-demo的默认项目中访问工作负载并选择【服务发现】标签。当所有在集群上的工作都完成之后,应该有5项服务在默认的命名空间内,有12项服务在istio-system命名空间内,并且所有这些服务都处于活跃状态。接着,选择grafana服务,并且从右边的菜单栏内选择 View/Edit YAML。

    image.png

    找到包含type: ClusterIP的那行,将其改为type: LoadBalancer,并点击【保存/Save】。然后它应该开始在Google Cloud Platform中配置负载均衡器,并在其默认端口3000上暴露Grafana。如果想要获取Grafana的公共IP地址的话,只需重复bookinfo示例中获取IP地址的步骤即可,即在API中查看grafana服务,你可以在其中找到IP地址,或通过kubectl获取它:

    image.png

    用你的浏览器访问:http://${GRAFANA_HOST}:3000/ ,选择其中一个面板,比如 Istio Service。通过此前应用的配置,我们限制了流量,仅显示版本1的reveiws应用。从服务的下拉菜单中选择 reviews.default.svc.cluster.local,就可以从图表中查看。现在使用以下命令从Rancher的kubectl生成一些流量:

    image.png

    需要等待约5分钟,为Grafana生成的流量将会显示在如下面板上:

    image.png

    如果你滚动面板,在SERVICE WORKLOADS下你将看到Incoming Requests by Destination And Response Code的图表,它要求Reviews应用程序只在v1端点结束。如果你使用以下命令,生成对版本2的应用的请求(请记得用户jason可以访问版本2的reviews 应用):

    image.png

    你应该也可以看到显示在版本2的应用上的请求:

    image.png

    用同样的方式,也可能可以暴露并且看到Istio其他默认的指标,比如Prometheus, Tracing 和ServiceGraph。

  • 还在篮子里

    《Cloud Native Go》 · 语雀

    《Cloud Native Go》

    测试

    • 为应用程序的整个生命周期内产生红利,特别是在生产环境中。
    • 创建失败测试 可 GitHub 搜索 FailPoint

    迭代测试-壮丽的蒙太奇

    1. TDD Pass 我们创建了测试 HTTP 务器所需的初始配置 该服务器调用
      HTTP 处理程序方法(被测方法) 由于被测方法尚不存在,测试开始时会编译失败。
      将测试资源代码添加到 createMatchHandler 方法中可以使测试通过。
    2. TDD Pass 添加断 ,判断 HTTP 返回值中是否包含 Location header
      初测试失败,因此在 location header 中添加了 个占位符
    3. TDD Pass 添加断 Location header 个正确格式的 URL ,指向由
      GUID 标识的 match 最初测试失败,随后生成 个新的 GUID 和设置正确的 location
      header
    4. TDD Pass 添加断言,判断 HTTP 返回值中 match ID location header
      中的 GUID 是否相等 最初测试失败,为了通过测试,需要在测试端添加能解析返
      回数据的代码 这意味着必须创建 个可以在服务器端返回有效数据的结构体。因
      此不在处理程序中返回“this is a test ”,而是返回 个真正的响应对象。
      68 Cloud Native Go
    5. TDD Pass 。添加断言,判断被处理函数使用的存储库是否已经包括了新创
      建的 tch 。为 ,必须 个存储库接口并实现 个内存存储库
    6. TDD Pass 。添加断言,判断服务返回的 大小和存储库中的是否相同。
      这使得我们必须为响应创建 个新的结构体,井进行更新。除此之外,我们还更新
      了另一个 gogo-engi 库,它实现了 游戏需要的最小分辨 的业务逻辑 应该
      在最大程度上与 GoGo 服务解祸。
    7. TDD Pass 添加断言 判断创建 请求时包含的游戏玩家和服务端的
      JSO 返回中的值是否相同,并且是否也相应地保存在存储库中
    8. TDD Pass 。添加断言,测试如果发送除 格式以外的数据,或者没有
      为创建 请求发送合理的值,服务器是否会返回“Bad Request 飞这些断言会失
      败,因此需要为处理程序添加检验 JSON 格式和无效请求对象的代码 Go 可以很好
      地支持 JSON 反序列化,所以通过检查反序列结构体中缺少的变量或默认值可以捕获
      大多数“ eq es ”输入。

    这是测试驱动开发的实例。作为一种敏捷开发方法,可以借鉴使用。

    服务发现和动态服务发现

    资料更新、多版本灰度发布。

    关于数据库编写

    • 测试驱动开发 编写数据库
    • 集成测试自动化测试 

    事件溯源

    事件溯源在我看来是一种非常棒的伴随开发的方法论。它注重调试,修改,迭代。我认为这也是软件开发中必要的部分。

    1. 幂等

    注重操作幂等,可以减少很多操作失败带来的损失

    1. 隔离

    单个事件独立,对于出错操作定位来说易于分析

    1. 可测试

    如果不可测试,都无法知道是否正确,就要用线上更大的成本检验。这是和开发本意不符合的。

    1. 可再现,可恢复

    通过这个原则,让出错,或者故意出错的环境能够再次出现,而不是在线上因为巧合崩溃,却不知道怎么修复,重启之后莫名其妙又好了这样的稀里糊涂的维护。

    1. 大数据

    我认为这里大数据指的是量化分析事件,能够屏蔽自己无法关注到的信息,直接找到被这些信息掩藏的因果关系。

    最后和现实最终一致

    事实上,这里有一个理念就是,编程出得程序,或者工具,它应该反应与现实一致。我认为这是约定大于配置的理念。

    借用 HackNew 里的一个人所说。

    当我发现,如果我只使用 Vim 的默认配置的话。我能够在所有的环境下使用 Vim 。

    而不是只能在我的定制化 vim 上操作。

    安全是一直考虑的问题

    另外要注意的是,安全是贯彻始终的一个问题。越早维护他,之后受到的损失越小。对待开发的软件一开始就认真对待,除了安全之外,质量也会有所保证。

    实践学科

    无论是编程,还是单纯云原生,都是如此。

    吾生也有涯,而知也无涯。以有涯随无涯,殆已。实践印证前进,而不是去了解所有的东西再着手。

    来源: 《Cloud Native Go》 · 语雀

  • 还在篮子里

    IPFS普及知识 · 语雀

    IPFS普及知识

    行星际文件系统任务

    InterPlanetary文件系统(IPFS)的目标是创建分布式Web。一种点对点超媒体协议,使网络更快,更安全,更开放。

    两个’中心’问题

    集中化带来了几个主要问题。第一个是当你有一个控制大量数据的中央公司时,如果无法访问这些服务器会发生什么?控制存储大量有价值和有用数据的服务器的中央公司单点故障导致完全无法访问。这种失败可能是由于攻击造成的,也可能是因为服务器离线了。

    将数据存储在中心位置的第二个问题是审查。当大多数数据托管在几个主要服务器上时,政府更容易阻止对它们的访问。2017年,土耳其居民无法访问Wikipedia.com。土耳其政府禁止该网站称其为“国家安全威胁”。这也发生在2011年1月的埃及,当时政府为95%以上的公民切断了互联网和手机接入。

    为什么我们使用有缺陷的系统?

    我们继续接受这种模式的真正原因是我们已经因为接入互联网而受到损害。我们希望网页以毫秒为单位加载,图像和视频无延迟地显示,当然,这一切都必须具有最高的HD或4K质量。集中服务器使公司可以完全控制他们提供此内容的速度,并相应地收取费用。我们继续这种方法的另一个原因是,确实没有一个好的选择。

    IPFS→好的选择

    InterPlanetary文件系统(也称为IPFS)是一种使Internet完全分布的想法。该概念将传统的“HTTP”互联网转变为对等网络,类似于BitTorrent的工作方式。

    Juan Benet是IPFS的创建者和Protocol Labs的创始人。Protocol Labs是一家负责IPFS的技术研发实验室,他们还开发了Filecoin和IPLD(以及其他)。Benet在斯坦福大学学习计算机科学,并且非常关注与知识,科学和技术有关的任何事情。

    Juan Benets最初的目标并不是在制作这个概念时必然会创建IPFS。他想要做的是找到一种有效的方法来移动科学数据集,这意味着数据大小可能是10-100 + GB。IPFS的设计看起来像Git和BitTorrent一起生孩子时会发生什么。BitTorrent使您能够快速地在网络上移动大型文件,Git为您提供内置的数据版本。

    在创建此协议之后,Benet很快意识到其影响远大于移动大型数据集。他实际上创建了一个协议,可以取代其他流行的协议,以便我们今天通过网络访问信息。

    InterPlanetary文件系统的名称来自哪里?

    当他们提出行星际文件系统(IPFS)的命名时,其想法是向互联网如何命名致敬。JCR Licklider是Arpanet(互联网的前身)的创造者,其目标是建立一个星际网络。因此,互联网对于星际网络而言确实很短。InterPlanetary采用相同的命名约定,此外,IPFS旨在成为Internet协议(IP)的文件系统(FS)。当把它们放在一起时,便有了IPFS,即互联网文件系统。我将深入探讨IPFS如何作为文件系统。但首先,了解我们今天如何从网络访问文件非常重要。

    当您想从互联网上下载图像时,您可以告诉您的计算机确切地找到您要求的图像的位置。此位置通常采用URL的形式,其中包含存储照片的公司的域名,后面跟着指定文件内容的扩展名。此博客文章的示例请求如下所示:https://achainofblocks.com/ipfs-simple-guide.jpg。这种访问资源的方法称为“基于位置的寻址”,您告诉计算机他们可以访问信息的位置,并且计算机检索信息。此方法的一个问题是,如果位置不可访问(可能服务器处于脱机状态),则用户的计算机无法检索所需的信息。

    服务器关闭,我们都倒闭了

    使用服务器关闭时基于位置的寻址,无法通过Internet访问该服务器中包含的所有内容。但是,当服务器发生故障时,另一个用户很可能已下载该图像,并将其本地存储在其计算机上。但即使另一台计算机确实有此文件,您的计算机也无法与这些计算机连接以传输该文件。

    关于内容

    为了帮助解决这个问题,IPFS引入了“基于内容的寻址”的概念。在请求特定资源时使用基于内容的寻址时,您无需指定位置,只需指定所需内容即可。

    每个文件都有一个唯一的哈希值,可以将其视为文件的指纹或标识。如果要访问特定文件,只需向网络询问具有指定哈希值的文件副本即可。请求完成后,IPFS网络上的某个人将提供您请求的资源。您将下载该资源,并将副本保存到您的IPFS缓存中。现在,当另一个人来并请求相同的文件时,您将能够提供给他们。这创建了一个系统,随着它的使用速度越来越快,因为共享的文件越多,它们就越容易获得,它们就是一组节点。

    有改变是好事…

    在这一点上,我和你有同样的问题:这个行为正确吗?我怎么知道提供给我文件的人或节点没有以某种方式篡改它?因为您使用哈希函数来检索文件,所以您可以验证收到的内容。更改文件的哈希值与更改区块链中的事务同样困难。请求具有特定散列的文件,因此在收到文件时,请确保散列与请求匹配。这与您用于验证亚马逊购买的方法相同。如果您订购了Green Socks,并且Red Socks出现,您将拒绝它们并等待您的Green Socks出现。

    IPFS的另一个功能是重复数据删除,这意味着当多个用户发布同一文件时,它只在网络上创建一次。这有助于提高网络效率。

    IPFS如何真正起作用

    现在您已经了解了IPFS与当今传统方法的比较基础,让我们更深入地了解IPFS如何实际存储数据并使其可供用户访问。

    在IPFS中,文件存储在IPFS对象中,每个Object可以存储256 kb的数据。对象还可以包含指向另一个IPFS对象的链接,链接可以存储大于256 kb的数据。例如,如果您只上传一个小文本文件,那么一个256 kb的对象应该适合您的少量文本。

    但是,如果要存储图片,则会将其分解为多个对象,每个对象最大为256 kb。然后,IPFS系统将创建一个空对象,该对象将链接到构成该图片的所有对象。

    这种架构非常简单,但它也非常强大,该架构真正使IPFS能够用作文件系统。如果你看一下下面的简单文件目录结构,我将解释如何将其转换为IPFS结构:

    通过为每个文件和每个文件夹/目录创建1个对象,然后将文件链接到指定的目录,可以将其转换为IPFS。如果考虑到IPFS使用基于内容的寻址的话会变得更好。这意味着添加的文件是不可变的,它们永远不会被更改,非常像区块链。这意味着访问的资源是正确的数据,并且从未被更改过。

    我如何更新我的数据?

    IPFS支持文件版本控制,这与Git如何作为开源代码库一样工作。例如,您正在处理名为“Important Document – v1.doc”的文本文件,并且您希望与使用IFPS的人共享此文档。当您将此文件添加到IPFS时,幕后发生的事情是,IPFS将创建一个新的Commit对象。这个Object是非常基本的,它只是告诉IPFS哪个Commit在此之前,它链接到与文件相关的IPFS对象,’Important Document – v1.doc’。

    现在让我们假设已经过了一段时间,你的’Important Document.doc’需要修改。只需将新文件添加到IPFS“Important Document – v2.doc”即可完成此操作,软件将为更新的文件创建新的提交对象(与原始进程相同)。此提交对象现在链接到先前的提交对象,第一个提交用作父对象。这个过程可以无休止地重复,创建一个链接的相同数据版本,所有这些都引用整个链。IPFS确保您的文件以及整个文件版本控制历史记录可供网络上的所有其他节点访问。

    没有系统是完美的

    到目前为止,我们已经讨论了InterPlanetary文件系统的许多有用功能和关键概念。但是,所有协议都有局限性和缺点。正如您可能想象的那样,IPFS目前面临的最大问题是保持文件可用。网络上的每个节点都保留了已下载文件的缓存,并有助于使其可用,因为其他用户需要它们。但是,在一个简单的情况下,如果一个Document由4个节点托管,并且它们全部脱机,则该文档不再可访问。

    有几种方法可以解决上述问题。一种方法是激励节点保持在线并使文件可供社区使用。对可以提交给网络的存储空间的奖励,这将确保文件很可能在需要时可用。另一种方法是主动在整个网络中分发文件,确保在任何给定时间总是有足够的在线副本。你可以把它想象成大规模的冗余。

    Filecoin

    这个问题是Filecoin旨在解决的确切问题。Filecoin由创建IPFS的同一组创建。Filecoin是一个建立在IPFS之上的区块链,其目标是创建一个分散的存储市场。这意味着在硬盘上有额外存储空间的用户可以将其出租用作IPFS存储,并在此过程中从中赚取一些钱。您可以将Filecoin视为与Airbnb类似的服务,而不是租用您家中的可用空间,您可以租用计算机上的可用空间进行存储。Filecoin为节点创建了一种激励,使数据保持在线并尽可能长时间保留。除了保持节点在线的激励之外,它还可以跨多个节点复制数据,使其高度可用且易于访问(即使少数节点处于脱机状态)。Filecoin和IPFS具有相同的目标,即脱机优先,这意味着他们不断努力获得更好的体验,而无需调用服务器来访问资源。

    这是Filecoin的一个非常高级的快速摘要。我将在未来的文章中深入研究Filecoin以及Protocol实验室中的一些其他伟大项目。

    行星际关联数据(IPLD)

    根据https://ipld.io

    “IPLD是内容可寻址网络的数据模型。它允许我们将所有与哈希相关的数据结构视为统一信息空间的子集,统一所有将数据与哈希作为IPLD实例链接的数据模型。“

    这意味着IPLD旨在成为可互操作协议的数据模型。这种技术有很多用例。这使得能够在IPFS上运行智能合约。关键是IPLD提供的库使底层数据可以跨工具和协议进行互操作。

    IPLD和Fileco都是非常复杂的项目,需要他们自己的专门文章才能完全理解。

    额外的想法

    我希望通过阅读本文显而易见,IPFS是一个非常雄心勃勃的项目。以权力下放为重点的大多数早期项目主要涉及货币和财务方面。IPFS实际上是建立一种更好的共享数据的方式。之前有过HTTP的挑战。然而,IPFS显然是最成熟的,并且被视为可以在未来几年中大规模采用的系统。我绝不暗示HTTP正在消失,Juan Benet自己也承认HTTP是一个很棒的协议,它仍然非常有用。但是,它是一个超过25年的协议,随着IPFS在采用中的不断增长,用例将随着技术的发展而扩展。在IPFS最终接管之前,我们很有可能会同时使用这两种协议。

    Reference

    https://achainofblocks.com/2018/10/05/ipfs-interplanetary-file-system-simply-explained/

    翻译整理:abser

    来源: IPFS普及知识 · 语雀

  • 还在篮子里

    acl 权限控制 · 语雀

    ACL 权限控制

    项目简介

    TL;DR;

    ACL is a lightweight ACL manager for Go.

    Features

    • Design simple & reusable roles to empower your application.
    • Acquire the rights of other roles to build a powerful set of permissions.
    • Resolve possible roles by examine them in an unified way.

    Example

    type User struct {
        isAdmin bool
    }
    
    func main() {
        // first of all: create a new manager instance to register all your roles in one place
        manager := acl.NewManager()
    
        // now you can use `Ensure` to guarantee that the role with the passed identifier is present
        user := manager.Ensure("user").Grant("profile.edit")
        // use `Grant`, `Revoke` and `AcquireFrom` to extend the right stack
        editor := manager.Ensure("editor").Grant("news.list", "news.create", "news.edit").AcquireFrom(user)
    
        // you can also use NewRole to create a Role manually
        admin := acl.NewRole("admin").Grant("news.delete").AcquireFrom(editor)
        // note, that you have to register the role by yourself
        manager.Register(admin)
    
        // to check if a right was granted to a role you can use:
        var hasAccess bool
        hasAccess = admin.Has("some.right")
    
        // to check if at least one of the expected rights is present:
        hasAccess = admin.HasOneOf("news.list", "news.create")
    
        // ... and finally, to check that all the expected rights are present, use:
        hasAccess = admin.HasAllOf("news.delete", "news.list")
    
        // a role can be extended with an examiner to determine whether a role can be added
        // to a `ResultSet`
        admin.SetExaminer(func (payload interface{}) bool {
            user := payload.(User)
            return user.isAdmin
        })
    
        // to get a result set you can use the managers `Examine` function
        rs := manager.Examine(User{isAdmin: true})
    
        // a result set contains "Has", "HasOneOf" and "HasAllOf" as described above and...
        // `GetRole` to grab specific roles from the result set
        expectedRole := rs.GetRole("admin")
    
        // you can also check if a role was added to a result set using:
        if rs.HasRole("admin") {
            // ...
        }
    }

    ##Blueprint

    来源: acl 权限控制 · 语雀