#858 日志平台的设计

2022-10-07

简单的日志系统

我还没有用过 ELK 这样的系统(只实验性使用过 Graylog),使用过这些日志管理方案:

  1. 日志就通过文件存放在服务器上,然后登录服务器进行日志文件分析,排查问题。
  2. 再进一步就是,将日志文件定期 rsync 到一台服务器上,方便日志管理与日志搜索。
  3. 服务直接写 syslog,然后配置好 rsyslog 同步就行。

    graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    
    A(应用程序):::process -->|写入日志| B(syslog 接口):::process
    B -->|发送日志消息| C(本地 rsyslog 守护进程):::process
    C -->|存储本地日志| D(本地日志文件):::process
    C -->|配置转发规则| E{是否转发到远程?}:::process
    E -->|是| F(通过 UDP/TCP 发送):::process
    F -->|接收日志消息| G(远程 rsyslog 守护进程):::process
    G -->|存储远程日志| H(远程日志文件):::process
    E -->|否| D
    
  4. 不要忘了,日志写入数据库在某些场景下也是一个可选方案,比如内部管理系统的登录日志、操作日志等。

理论上日志可以直接写入远程日志系统,但是我想应该不会有线上服务这样做。网络稳定性问题(可能丢失日志、服务阻塞)、性能消耗、增加系统复杂性等。
syslog 是系统提供的日志接口,rsyslog 提供了 TCP 日志可靠传输、本地日志队列功能,而且是 Linux 世界广泛采用的基础服务,我觉得可以接受。

日志系统

  • ELK / EFK:

    Elasticsearch   # 搜索引擎
    Logstash        # 日志采集、过滤、预处理
    Kabana          # 数据可视化
    
    Filebeat        # Logstash 替代方案,更加轻量级
    
    graph LR
        Logs --> Filebeat --> ES
        Logs --> Logstash --> ES
    
  • FELK:Filebeat 将日志采集到 Logstash,处理之后导入 ES

    graph LR
        Logs --> Filebeat --> Logstash --> ES
    
  • FELK + Kafka

    Filebeat 将日志采集到 Kafka,再由 Logstash 从 Kafka 读取日志,处理完成之后导入 ES

    graph LR
        Logs --> Filebeat --> Kafka --> Logstash --> ES
    
  • Graylog:

    Filebeat
    Graylog Sidebar
    Graylog
    Elasticsearch
    MongoDB
    
  • LPG:

    Loki        # 搜索引擎
    Promtail    # 日志采集、过滤、预处理
    Grafana     # 数据可视化
    

设计

graph TD
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A(服务):::process -->|生成日志| B(rsyslog):::process
    B --> C(Kafka):::process
    C --> D(ELK):::process
  1. 服务写 syslog,通过 rsyslog 同步到

接收,解析,处理,转换,格式化

#857 gob:Golang 二进制编码格式(标准库)

2022-10-01
package main

import (
    "bytes"
    "encoding/gob"
    "fmt"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    // 编码
    var buf bytes.Buffer
    enc := gob.NewEncoder(&buf)
    err := enc.Encode(Person{"John", 30})
    if err != nil {
        panic(err)
    }

    // 解码
    dec := gob.NewDecoder(&buf)
    var p Person
    err = dec.Decode(&p)
    if err != nil {
        panic(err)
    }

    fmt.Println(p)
}

#856 Golang 源码结构

2022-09-16
# tree -L 1 -d .
.
├── api
├── doc
├── lib
├── misc
├── src
└── test

6 directories
# tree -L 1 -d src/
src/
├── archive
├── bufio
├── builtin
├── bytes
├── cmd
├── compress
├── container
├── context
├── crypto
├── database
├── debug
├── embed
├── encoding
├── errors
├── expvar
├── flag
├── fmt
├── go
├── hash
├── html
├── image
├── index
├── internal
├── io
├── log
├── math
├── mime
├── net
├── os
├── path
├── plugin
├── reflect
├── regexp
├── runtime
├── sort
├── strconv
├── strings
├── sync
├── syscall
├── testdata
├── testing
├── text
├── time
├── unicode
├── unsafe
└── vendor

46 directories
tree -L 2 -d src/net/
src/net/
├── http
│   ├── cgi
│   ├── cookiejar
│   ├── fcgi
│   ├── httptest
│   ├── httptrace
│   ├── httputil
│   ├── internal
│   ├── pprof
│   └── testdata
├── internal
│   └── socktest
├── mail
├── rpc
│   └── jsonrpc
├── smtp
├── testdata
├── textproto
└── url

19 directories

#855 Golang UUID

2022-09-13

awesome-go 上面列出来的项目:

  • jakehl/goid  - Generate and Parse RFC4122 compliant V4 UUIDs.
  • twharmon/gouid  - Generate cryptographically secure random string IDs with just one allocation.
  • aidarkhanov/nanoid  - A tiny and efficient Go unique string ID generator.
  • muyo/sno  - Compact, sortable and fast unique IDs with embedded metadata.
  • oklog/ulid  - Go implementation of ULID (Universally Unique Lexicographically Sortable Identifier).
  • uniq - No hassle safe, fast unique identifiers with commands.
  • agext/uuid  - Generate, encode, and decode UUIDs v1 with fast or cryptographic-quality random node identifier.
  • gofrs/uuid  - Implementation of Universally Unique Identifier (UUID). Supports both creation and parsing of UUIDs. Actively maintained fork of satori uuid.
  • google/uuid  - Go package for UUIDs based on RFC 4122 and DCE 1.1: Authentication and Security Services.
  • edwingeng/wuid  - An extremely fast globally unique number generator.
  • rs/xid  - Xid is a globally unique id generator library, ready to be safely used directly in your server code.

我自己又在 GitHub 上搜罗了几个:

package main

import (
    "os/exec"

    guuid "github.com/google/uuid"
    suuid "github.com/satori/go.uuid"
)

func UseGoogle() string {
    id := guuid.New()
    return id.String()
}

func UseSatori() string {
    id := suuid.NewV4()
    return id.String()
}

func UseUuidgen() string {
    id, _ := exec.Command("uuidgen").Output()
    return string(id)
}

func main() {
    fmt.Println("Google UUID:", UseGoogle())
    fmt.Println("Satori UUID:", UseSatori())
    fmt.Println("Uuidgen UUID:", UseUuidgen())
}

#854 Go 谚语

2022-09-13

得知了 Go 谚语这么个东西,搜索一番,找到:

  • https://go-proverbs.github.io/
  • https://github.com/jboursiquot/go-proverbs
  • https://www.youtube.com/watch?v=PAAkCSZUG1c

原来是类似 Python 之禅(import this)一样的东西。

  1. Don't communicate by sharing memory, share memory by communicating. 别通过共享内存通信,要通过通信共享内存。
  2. Concurrency is not parallelism. 并发不是并行。
  3. Channels orchestrate; mutexes serialize.
  4. The bigger the interface, the weaker the abstraction. 接口越大, 抽象越弱。
  5. Make the zero value useful.
  6. interface{} says nothing.
  7. Gofmt's style is no one's favorite, yet gofmt is everyone's favorite. 别争论风格问题,不管喜不喜欢,用 gofmt 就是了。
  8. A little copying is better than a little dependency. 少量的复制好过引入新的依赖。
  9. Syscall must always be guarded with build tags.
  10. Cgo must always be guarded with build tags.
  11. Cgo is not Go. 别用 cgo。
  12. With the unsafe package there are no guarantees. 慎用 unsafe 包(安全无保障)
  13. Clear is better than clever. 清晰的代码比聪明的代码好。
  14. Reflection is never clear. 慎用反射(代码不清晰)。
  15. Errors are values.
  16. Don't just check errors, handle them gracefully.
  17. Design the architecture, name the components, document the details.
  18. Documentation is for users. 文档是给用户的(注意代码本身的可读性)。
  19. Don't panic. 不要滥用 panic

#853 Protobuf 枚举类型的实现

2022-09-12
enum Gender {
    UNKNOWN = 0;
    MALE = 1;
    FEMALE = 2;
}

转换成 Golang 之后是这个样子:

type Person_Gender int32

const (
    Person_UNKNOWN Person_Gender = 0
    Person_MALE    Person_Gender = 1
    Person_FEMALE  Person_Gender = 2
)

// Enum value maps for Person_Gender.
var (
    Person_Gender_name = map[int32]string{
        0: "UNKNOWN",
        1: "MALE",
        2: "FEMALE",
    }
    Person_Gender_value = map[string]int32{
        "UNKNOWN": 0,
        "MALE":    1,
        "FEMALE":  2,
    }
)

func (x Person_Gender) Enum() *Person_Gender {
    p := new(Person_Gender)
    *p = x
    return p
}

func (x Person_Gender) String() string {
    return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}

func (Person_Gender) Descriptor() protoreflect.EnumDescriptor {
    return file_example_proto_enumTypes[0].Descriptor()
}

func (Person_Gender) Type() protoreflect.EnumType {
    return &file_example_proto_enumTypes[0]
}

func (x Person_Gender) Number() protoreflect.EnumNumber {
    return protoreflect.EnumNumber(x)
}

// Deprecated: Use Person_Gender.Descriptor instead.
func (Person_Gender) EnumDescriptor() ([]byte, []int) {
    return file_example_proto_rawDescGZIP(), []int{0, 0}
}

#852 Protobuf 二进制编码

2022-09-12

相较于文本编码的 JSON

  • 速度快(序列化和反序列化)
  • 数据小(二进制数据流更加紧凑)
  • 数据类型丰富(enum,map...)
  • 可读性差

数据类型

Protobuf 类型 描述 Go 对应类型
int32 有符号 32 位整数,采用可变长度编码,适合较小的整数 int32
int64 有符号 64 位整数,采用可变长度编码 int64
uint32 无符号 32 位整数,采用可变长度编码 uint32
uint64 无符号 64 位整数,采用可变长度编码 uint64
sint32 有符号 32 位整数,采用 ZigZag 编码,适合负数较多的情况 int32
sint64 有符号 64 位整数,采用 ZigZag 编码 int64
fixed32 无符号 32 位整数,固定 4 字节编码,对于大整数比 uint32 更高效 uint32
fixed64 无符号 64 位整数,固定 8 字节编码 uint64
sfixed32 有符号 32 位整数,固定 4 字节编码 int32
sfixed64 有符号 64 位整数,固定 8 字节编码 int64
float 单精度 32 位浮点数 float32
double 双精度 64 位浮点数 float64
bool 布尔值,取值为 truefalse bool
string 包含 UTF - 8 编码或 7 位 ASCII 文本的字符串,长度不能超过 2^32 string
bytes 任意的字节序列,长度不能超过 2^32 []byte
message 自定义的数据结构,由多个字段组成,可以嵌套定义 struct
enum 用于定义一组命名的常量值 Go 枚举实现
repeated 在字段前加上 repeated 关键字表示该字段是一个数组 Go 数组
map<KeyType, ValueType> 从 proto3 开始支持的映射类型,用于表示键值对 map
  1. 枚举类型,有另一篇文章可供参考:Protobuf 枚举类型的实现

  2. 数组(Repeated)示例:

    message Person {
        repeated string hobbies = 5;
    }
    
  3. 映射(Map)示例:

    message Person {
        map<string, string> contacts = 6;
    }
    

    这里 contacts 是一个键为字符串、值也为字符串的映射。

基本流程

  1. 定义数据结构(.proto 文件)
  2. 使用编译器生成目标编程语言代码
  3. 在程序中引入生成的代码

  4. Protobuf 编译器: protoc,作用是生成目标语言的数据定义代码
    生成的代码
    需要到 GitHub 下载。

  5. Protobuf 运行时,作用是序列化和反序列化的基础库

    Lang Repo
    C++ https://github.com/protocolbuffers/protobuf/blob/main/src
    Java https://github.com/protocolbuffers/protobuf/blob/main/java
    Python https://github.com/protocolbuffers/protobuf/blob/main/python
    Golang https://github.com/protocolbuffers/protobuf-go

示例

  1. proto 文件

    syntax = "proto3";
    
    package example;
    
    message Person {
        enum Gender {
            UNKNOWN = 0;
            MALE = 1;
            FEMALE = 2;
        }
        string name = 1;
        int32 age = 2;
        Gender gender = 3;
    }
    
  2. 编译:

    $ protoc --go_out=. --go_opt=Mexample.proto=./generated example.proto
    $ find .
    .
    ./example.proto
    ./generated
    ./generated/example.pb.go
    
  3. 调用 proto 生成的 Go 代码

    package main
    
    import (
        "fmt"
        "log"
    
        pb "example/generated" // 导入生成的包
    
        "google.golang.org/protobuf/proto"
    )
    
    func main() {
        // 创建一个 Person 实例
        person := &pb.Person{
            Name:   "Alice",
            Age:    30,
            Gender: pb.Person_FEMALE,
        }
    
        // 序列化
        data, err := proto.Marshal(person)
        if err != nil {
            log.Fatalf("Failed to marshal: %v", err)
        }
    
        // 反序列化
        newPerson := &pb.Person{}
        err = proto.Unmarshal(data, newPerson)
        if err != nil {
            log.Fatalf("Failed to unmarshal: %v", err)
        }
    
        fmt.Printf("Deserialized person: %+v\n", newPerson)
    }
    
    $ go mod init example
    $ go mod tidy
    
    $ go build -o main.exe .
    $ ./main.exe
    Deserialized person: name:"Alice" age:30 gender:FEMALE
    

#851 HSTS: HTTP Strict Transport Security

2022-09-04

HSTS 全称 HTTP Strict Transport Security,中文就是 HTTP 严格传输安全

HSTS 的作用是通过特定的 HTTP 响应头告知浏览器,未来访问该网站时必须使用 HTTPS 连接,确保通信的安全性。这可以避免用户通过 HTTP 访问时的自动重定向,从而减少延迟。

对于同时支持 HTTP 和 HTTPS 的站点,HSTS 强制客户端始终使用 HTTPS,防止潜在的中间人攻击。主流浏览器都已全面支持 HSTS。

HSTS 于 2012 年成为互联网标准建议(RFC 6797),并逐渐被广泛采用。

背景

SSL 剥离攻击(SSLStrip),HTTP 降级攻击

受益于一整套相对可靠的公钥基础设施(PKI),直接的 HTTPS 通信是无法中间人攻击的。
中间人的伪造证书会被校验出来。

但是部分网站都是配置 HTTP 和 HTTPS 地址共存。

工作原理

通过 Strict-Transport-Security 响应头(下面称之为 HSTS 头),告诉客户端应该采用 HTTPS 连接。
客户端以后再访问这个网站就会自动重定向到 HTTPS(即使指定了 HTTP 协议)。

语法

Strict-Transport-Security: max-age=<expire-time>
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains; preload
  • max-age 作用时长(秒),表示在这么长的时间之内都应该直接请求 HTTPS 协议。
  • includeSubDomains 对所有子域名生效
  • preload 添加到 HSTS 预加载列表需要的
    • 不是标准的一部分,但主流浏览器都支持

例如:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

表示两年之内,当前域名以及所有子域名,都通过 HTTPS 访问。

预加载列表

要求原文:

  1. Serve a valid certificate.
  2. Redirect from HTTP to HTTPS on the same host, if you are listening on port 80.
  3. Serve all subdomains over HTTPS.
    1. In particular, you must support HTTPS for the www subdomain if a DNS record for that subdomain exists.
  4. Serve an HSTS header on the base domain for HTTPS requests:
    1. The max-age must be at least 31536000 seconds (1 year).
    2. The includeSubDomains directive must be specified.
    3. The preload directive must be specified.
    4. If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS header (rather than the page it redirects to).

问题

  1. 第一次访问不受 HSTS 保护
  2. 如果网站想回退到 HTTP,可能会受阻

Nginx HSTS 配置

  1. 80 rewrite 到 https

    server {
        listen 80;
        server_name example.com;
        # rewrite ^(.*)$ https://$host$1 permanent;
        return 301 https://$server_name$request_uri;
    }
    
  2. HSTS 头

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
    

PKP:Public Key Pinning

和 HSTS 类似,服务器告诉浏览器网站的证书指纹,浏览器缓存起来。
后面的每次访问都会计算证书指纹,如果和之前缓存的哈希不匹配,则向用户发出警告,存在中间人攻击风险。

计算证书指纹的方法:

  1. SPKI
  2. 哈希,对整个证书计算哈希值,更加安全

和 HSTS 类似的是工作方式,目的(提升安全性),不同的是手段,HSTS 是为了确保采用安全的协议,PKP 是确保证书不被伪造。

参考资料与拓展阅读

#850 转载:USB 的前世今生

2022-08-30

在人类的历史长河中,很少有一种技术或者传输标准能像USB那样跟我们的生活息息相关,甚至到了没有不行的地步。
USB对于今天的人们来说,就好像是空气,是水,是我们每天必需但是又熟视无睹的东西,没有多少人知道它从哪来,也没多少人关心它要往哪去,对于大多数人来说,它平凡得不能再平凡了。
但是,在我们“电子攻城狮”的眼里,它太有趣了,它是目前使用率最高的接口,它是我们身边林林总总电子设备之间的高速公路。
因此我们必须关注它,如果有必要,我们还不得不去了解如何才能实现它。作为一个USB开发者(电子爱好者),接下来我会为大家揭开USB神秘的面纱,带大家去了解USB是怎么出现并且逐渐演化的,以及在它出现后给我们带来了什么。

#849 关于 Linux 发行版

2022-08-28

发行版

  • Minix
  • Unix 阵营
  • 闭源
    • HP-UX
    • Solaris
    • AIX
    • macOS
  • 开源
    • FreeBSD
    • NetBSD
    • OpenBSD
    • DragonFly BSD
  • Linux 阵营
  • Android (AOSP)
    • LineageOS
  • Debian
    • Ubuntu
  • RedHat
    • CentOS
  • OpenSUSE
  • Gentoo
  • Arch Linux
    • Manjaro

Debian / Ubuntu 和 RHEL / CentOS 衍生版本太多,就不列了。
我们常见的 Linux 发行版应该差不多都能找到自己的位置。

桌面环境

图形库主要就是两种:GTK,Qt

  • 基于 GTK
  • GNOME
  • Xfce
  • Cinnamon
  • MATE
  • LXDE
  • Unity:ubuntu
  • Budgie
  • 基于 Qt
  • KDE Plasma
  • LXQt
  • Deepin Desktop Environment (DDE)
  • UKUI:ubunty kylin

个人看法

  1. 在国际关系越来越趋于对抗的这个局面下,Linux 对于中俄等国都有战略安全的重要作用。
  2. 国产 Linux 系统厂商大有搞头。
  3. 关于国产系统:
    1. 国产 Linux 桌面只有 Deepin 可以一战,而且做了太多工作,应该得到尊敬。
    2. 国产服务器系统我一个都没有用过,不知道如何。
  4. Unix 系统的生态和 Linux 完全不是同一个级别。
  5. 除非有特别的原因,否则应该直接选择 Linux。
  6. Deepin 等国产 Linux 系统提供商想搞什么国产根系统,想切断和社区的关联,在我看来是非常愚蠢的。
  7. 反而应该加大对社区的投入。
  8. 如果不知道该选哪个 Linux 发行版,我的建议是 Ubuntu。
  9. 无论是服务器还是个人使用,都可以直接选择上一个版本 Ubuntu LTS
  10. 如果比较喜欢尝鲜,个人使用可以选最新版本 Ubuntu
  11. 如果对 Ubuntu snap 不满意,就选择 Debian。
  12. 如果问选哪个桌面环境,就选 GNOME
  13. KDE 我没有用过,不知道
  14. 选 GNOME 作为默认桌面的多,应该是有道理的
  15. 桌面并没有那么重要,方便快捷的命令行操作才是 Linux 精髓
  16. 如果对发行版不满,就在基准系统上通过增删组件达成自己的目的,不要觉得换一个系统就能完美解决问题。
  17. 如果有时间折腾不同的发行版,还不如去玩一会儿游戏。