GithubHelp home page GithubHelp logo

cherish-today's People

Contributors

wang-kai avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

cherish-today's Issues

正向代理与反向代理

正向代理

正向代理服务让 LAN 客户机接入 WAN 以访问外网资源,正向代理服务器不支持外部对内部网络的访问请求。正向代理的目的如下:

  1. 增强局域网内部网络的安全性,使得网外的威胁因素不容易影响到网内,这里的代理服务起到了一部分防火墙的功能。
  2. 利用代理服务也可以对 LAN 向 WAN 的访问进行必要的监控与管理。

Proxy

反向代理

反向代理服务器用来让外网客户端接入局域网中的站点以访问站点的资源。

反向代理

如何获取请求 IP 的地址呢?

一个 HTTP 请求发送到目标网站,要经过 NAT 变更 IP 地址、网关代理、负载均衡等层层转发,那么如何获取来源真实 IP 呢?客户端的内网 IP 地址肯定是获取不到了,即使获取到也没有任何用,其出网 IP 才具有可追溯的意义。请求经过代理、负载均衡等到达应用服务后,应用服务获取的 RemoteIP 仅仅是 上一跳 Client 的 IP 地址,大概率是个内网地址,获取源 IP 地址需要通过 HTTP X-Forworded-For header 来获取。该头信息会记录每次一次转发的 IP 地址,IP 之间以 , 分隔。

具体代码实现如下:

func GetRealIP(r *http.Request) string {
	IP := net.ParseIP(strings.Split(r.Header.Get("X-Forwarded-For"), ", ")[0]).String()

	if IP == "" {
		IP = r.RemoteAddr
	}

	return IP
}

nginx 要添加 XFF 头的话,需要特殊配置:

location / {
    proxy_pass http://127.0.0.1:8765;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

use gRPC with JSON format

之前源哥在开会上想要介绍一下我们的 gateway,认为这个基于反射技术的 gateway 是有技术创新性的,被我打断说这个技术没有创新性,2年前我就见到 Github 上有类似的。经过我 google 再三查找,并对所找到的方案做分析,认为源哥的实现方式还是最具通用性和稳妥的。

会后我找了 grpc-json-proxy,也能做类似的事情,这个项目是 inspired by grpc-json-example,grpc-json-example 作者在 2018 年 7月有一篇博客,专门介绍了他的实现方式。他发现:

  1. grpc-go 提供了 codec 的注册机制,可以注册其他的编码、解码方式,也就是说可以注册一个按照 JSON 格式的编解码器
  2. gRPC payload 有固定的格式,第一位是个 boolean 值,标记 payload 内容是否被压缩,第 2~5位标记内容长度。于是乎,客户端就可以把 JSON 格式的数据前边加上必要的 5 位信息值就可以被 gRPC server 所使用,并使用 JSON codec 来解析

这种实现有其优势,比如如此方式实现的 gateway 将不再需要后端提供 proto 文件,因为其不需要反射,后端服务改动接口,gateway 甚至不需要重启。因为其职责是加工处理下请求体、响应体,并且根据请求将其路由到对应的后端服务。

但这种方式有其很大的弊端:

  1. 服务端必须注册 JSON codec
  2. 调用该 backend server 的其他后端服务,也需要在创建客户端的时候加上 grpc.CallContentSubtype(codec.JSON{}.Name()) 特殊的 option

这相当于使用了 gRPC 的通信方式,但没有使用 protobuf 这样的序列化载体。第一舍弃了 protobuf 这种精髓的技术,第二使得这个模块不通用,它提供的已经不再是通用的 gRPC 接口了。

K8S 问题集锦

1. 当 Deployment 不指定副本数量的时候,默认副本数量是多少?

2. Pod 中是否可以两个 ContainerPort 端口一样?

3. StatefulSet 如何连接?

Golang 枚举使用

Go 语言没有 enum 关键字的,通过使用 const & iota 可以实现枚举的能力。本篇文章将探讨几个问题:

  1. 为什么要使用枚举,没了它就不行嘛?
  2. 如何在 Go 语言中优雅的使用枚举。

为什么要使用枚举?

Stackoverflow 上有个问题 What are enums and why are they useful? 中的回答很具备说服力。

当一个变量(尤其是一个方法的参数)仅能取自一个很小的选择集合中时,就应该使用枚举。例如类型常量(合同状态: "permanent", "temp", "apprentice")或者标记(“执行中”、“延后执行”)等。

当使用枚举去替代整数时,运行时会去检查传入的参数是否是合法参数(是否在定义的枚举集合当中),避免错误的传入了一个不可用的常量。

举例来讲,第一种实现,通过文档来备注每个数字的含义:

/** Counts number of foobangs.
 * @param type Type of foobangs to count. Can be 1=green foobangs,
 * 2=wrinkled foobangs, 3=sweet foobangs, 0=all types.
 * @return number of foobangs of type
 */
public int countFoobangs(int type)

调用该方法的时候:

int sweetFoobangCount = countFoobangs(3);

通过文档来备注每种状态的数字代号这种方案,在大型开发中着实是让人头疼的,况且并不见得文档中写的和代码中实际是一致的。人员流动交接常常会遗漏许多东西,慢慢的谁都不愿意再来维护这个项目。但使用枚举来实现的话,就变得清晰易懂,且避免了出错。

/** Types of foobangs. */
public enum FB_TYPE {
 GREEN, WRINKLED, SWEET, 
 /** special type for all types combined */
 ALL;
}

/** Counts number of foobangs.
 * @param type Type of foobangs to count
 * @return number of foobangs of type
 */
public int countFoobangs(FB_TYPE type)

调用方法的时候:

int sweetFoobangCount = countFoobangs(FB_TYPE.SWEET);

这种方案就很清晰,代码自带说明性,开发 & 维护起来都很方便。

如何在 Go 语言中使用枚举?

如开篇所言,Go 语言中没有 enum 类型,但我们可以通过 const & iota 来实现。go 源码中有一段就是很好的示例代码。使用步骤如下:

  1. 定义枚举类型
  2. 设定该枚举类型的可选值集合,可以借助 iota 的能力来简化赋值流程
type FileMode uint32

const (
	// The single letters are the abbreviations
	// used by the String method's formatting.
	ModeDir        FileMode = 1 << (32 - 1 - iota) // d: is a directory
	ModeAppend                                     // a: append-only
	ModeExclusive                                  // l: exclusive use
	ModeTemporary                                  // T: temporary file; Plan 9 only
	ModeSymlink                                    // L: symbolic link
	ModeDevice                                     // D: device file
	ModeNamedPipe                                  // p: named pipe (FIFO)
	ModeSocket                                     // S: Unix domain socket
	ModeSetuid                                     // u: setuid
	ModeSetgid                                     // g: setgid
	ModeCharDevice                                 // c: Unix character device, when ModeDevice is set
	ModeSticky                                     // t: sticky
	ModeIrregular                                  // ?: non-regular file; nothing else is known about this file

	// Mask for the type bits. For regular files, none will be set.
	ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular

	ModePerm FileMode = 0777 // Unix permission bits
)

最后再着重说一下 iota 的用法。

  1. iota 代表了一个连续的整形常量,0,1,2,3 ...
  2. iota 将会被重置为 0 ,当再一次和 const 搭配使用的时候
  3. iota 所定义的值类型为 int,它会在每次赋值给一个常量后自增

Linux 磁盘挂载

1. 对磁盘做格式化操作

// ext4 文件系统
mkfs.ext4 /dev/vdb

// xfs 文件系统
mkfs.xfs /dev/vdc

2. 将磁盘挂载到指定目录

mount /dev/vdc /data

3. 卸载磁盘

umount /dev/vdc

修改 /etc/fstab

操作系统启动的时候会根据 /etc/fstab 配置来加载磁盘,为了避免重启后磁盘丢失,需修改 fstab

# fstab
/dev/vdb        /data   xfs     defaults,nofail 0       0

主机 IP 来源探究

Q:

两个主机,均有外网 IP、内网 IP。A 通过外网 IP 访问 B,B RemoteIP 显示的是 A 的外网 IP,A 通过 内网 IP 访问 B,B RemoteIP 显示的是 A 的内网 IP,为什么?

A:

补充一个网络知识 NAT,在上世纪 90 年代,可用的 IPv4 正面临枯竭的威胁。除了 IPv6 之外,一种最为重要的机制就是网络地址转换(NAT)。NAT 本质上是一种允许在互联网的不同地方重复使用相同 IP 地址集的机制,现在它已被大多数网络路由器所支持。

如下 IP 地址专供内网使用,不会出现在 Internet 中:

  1. 10.0.0.0/8
  2. 172.16.0.0/12
  3. 192.168.0.0/16

NAT 的工作原理就是重写通过路由器的数据包识别信息。这种情况发生在数据传输的两个方向上。NAT 会重写往一个方向传输的数据包的源 IP 地址,重写往另一个方向传输的数据包的目的 IP 地址。这允许传出的数据包的源 IP 地址变为 NAT 路由器中面向 Internet 的网络接口地址,而不是原始主机的接口地址。因此,在互联网上的主机看来,数据包来自具备全局路由 IP 的 NAT 路由器,而不是位于 NAT 内部的私有地址的主机。

IP报文在经过 router 的时候,router NAT 功能会修改数据包中的 IP & Port,把内网 IP 替换成可以在 Internet 上路由的 IP,并创建新端口,替换掉源端口。

等回包的时候再查看路由表,确定转发到哪个内网IP 的哪个端口,这就是 NAT(网络地址转换)。这就做到了一个外网 IP 支持了局域网上多个内网电脑的上网需求。

xfs 设置磁盘配额(quota)

操作简介:

挂载目录的时候开启针对于 users, groups, and/or projects 选项,然后可以使用 xfs_quota 命令来限制或者查看 quota。

开启 quota

开启用户 quota

mount -o quota /dev/xvdb1 /xfs

开启针对于用户组的 quota

mount -o gquota /dev/xvdb1 /xfs

开启针对于项目的配额

mount -o prjquota /dev/xvdb1 /xfs

查询 quota 信息

xfs_quota 工具可以用来设置和查看 quota 信息,它有许多子命令。-c 后可以接 subcommands。任何需要通过命令行改动配额系统的,都需要添加 -x 参数。

1. 限制用户 quota

xfs_quota -x -c 'limit -u bsoft=5m bhard=6m john' /xfs

2. 查看磁盘的 quota 信息

xfs_quota -c print
Filesystem          Pathname
/                   /dev/vda1
/foo                /dev/vdb (uquota)

可知,/dev/vdb 盘设置了针对用户的 quota 。

3. 查看文件系统的配额信息

-h means human readable

xfs_quota -x -c 'report -h' /foo
User quota on /foo (/dev/vdb)
                        Blocks
User ID      Used   Soft   Hard Warn/Grace
---------- ---------------------------------
root            0      0      0  00 [------]
zhihu          4K      0      0  00 [------]
john           6M     5M     6M  00 [6 days]

4. 查看 inode & block 使用情况

blocks (-b) and inodes (-i)

xfs_quota -x -c 'free -hb'

xfs_quota -x -c 'free -hi'

为什么不推荐直接在 Docker container 中写操作?

容器内写操作流程

The major difference between a container and an image is the top writable layer.

如文档中所言,Container 比 Image 多的,就是最上面一层可写层。Container 启动时会在镜像最顶层加一层 R/W layer。多个 Container 会共享基础镜像层,那么想修改一个文件怎么办呢?比如我想修改操作系统的 /etc/hosts 文件。

sharing-layers

writable layer 是非常轻量级的,所有 container 对文件系统的改动都将存储于此。当要修改 /etc/hosts 文件时,存储驱动将触发 copy-on-write 操作,该操作大致有如下流程:

  1. 从顶层到基础层,查找 /etc/hosts 文件,被找到后将被添加到缓存,以提升未来的操作速度
  2. 执行 copy_up 操作,将 /etc/hosts 文件拷贝到可写层
  3. 对可写层的 /etc/hosts 做改动,container 对于底层已经存在的 /etc/hosts 将是不可见的

copy_up 操作会有明显的性能损耗,但该性能损耗仅在第一次文件修改的时候发生。因为可写层已经有了该文件

综述

Docker container 奉行 copy-on-write 的策略,写之前要经过查找、复制的流程,所以会大大降低写性能,所以不推荐在 Docker container 中做 I/O 密集的操作。

尽管如此,Docker 的 copy-on-write 策略有很大的优势:

  1. 节省磁盘使用空间,为每个 container 创建一个很薄的可写层,而不是 Copy 整个基础镜像。
  2. 提升启动时间,创建轻量级的 writable layer 肯定是很快的。

Go 之 简短声明语法糖的使用注意点

:= 是我在学习 Go 的时候觉得挺怪的一个语言词法,因为我从 Java & Javascript 过来。但 := 确实是 Go 引以为傲的的一个语法糖,甚至成为其标志性的语法。本文总结一下 := 需要谨记的几个坑。

1. 必须在函数内部使用

:= 相当于 声明并赋值,该操作必须放在函数内。

2. 不可重复声明

func f(i int) {
    i := 4
    println(i)
}

这里会报编译错误,因为传参的时候 i 已被声明,而函数中再次声明,所以编译报 no new variables on left side of := 错误。

那么,如果使用 := 同时操作多个变量,有的已被声明过,有的是新的变量,那么新的变量是声明并赋值,另外已在此作用域被声明过的变量就是做第二次赋值了。

3. 作用域问题

package main

func main() {
	i, j := 1, 2

	if true {
		i, k := 3, 4

		println(i, k)   // 3 4
	}

	println(i, j)   // 1 2
}

if true {} 创建了新的作用域,在该作用域内外,有同名变量 i , 它们是两个不同的变量。

基于 Redis 实现分布式琐

读了文章 https://redis.io/topics/distlock 大致了解了一下基于 Redis 的分布式琐实现,当然只是读了文章,未尝自己实现,也没有看已实现的 library 代码。

单 Redis 实例场景

获取琐 SET resource_name my_random_value NX PX 30000。通过 Redis NX & PX 确保同一资源名只能被一个客户端赋值,且该值有过期时间,避免了造成死锁。但该方案会有单点失败的问题。

多 Master 节点实现

多 Master 节点实现,而非一主多从。因为假使是一主多从,第一 Redis 的复制是异步的,抢琐的时候 slave 节点可能还没有同步到数据。第二,假使主节点挂掉,从节点变为主节点这种情况也是很复杂的。

Redlock 算法的流程:

  1. It gets the current time in milliseconds.
  2. It tries to acquire the lock in all the N instances sequentially, using the same key name and random value in all the instances. During step 2, when setting the lock in each instance, the client uses a timeout which is small compared to the total lock auto-release time in order to acquire it. For example if the auto-release time is 10 seconds, the timeout could be in the ~ 5-50 milliseconds range. This prevents the client from remaining blocked for a long time trying to talk with a Redis node which is down: if an instance is not available, we should try to talk with the next instance ASAP.
  3. The client computes how much time elapsed in order to acquire the lock, by subtracting from the current time the timestamp obtained in step 1. If and only if the client was able to acquire the lock in the majority of the instances (at least 3), and the total time elapsed to acquire the lock is less than lock validity time, the lock is considered to be acquired.
  4. If the lock was acquired, its validity time is considered to be the initial validity time minus the time elapsed, as computed in step 3.
  5. If the client failed to acquire the lock for some reason (either it was not able to lock N/2+1 instances or the validity time is negative), it will try to unlock all the instances (even the instances it believed it was not able to lock).

大致就是 N 个 Master 节点,只要能获取 N/2 +1 个节点的琐,就算获取了琐。获取琐最好是多线程执行,并且设置超时时间,避免在与一个节点通讯时花费大量时间。比如过期时间为 10s,那么可以设置 timeout 时间为 40ms。

Postgres Indexs

postgres 包含多种类型的索引:B-tree, Hash, GiST, SP-GiST, GIN and BRIN,B-tree 是默认的索引类型。

GIN

GIN 适合于包含多部分的值,例如 Array

MySQL 常用奇技淫巧

1. 显示创建表时的 SQL 语句

show create table <table_name>;

2. 优雅的呈现查询结果(适合字段内容比较长的场景)

select * from t_job \G;
*************************** 1. row ***************************
             id: 6
         job_id: steve_201902231906130001
      src_files: everyday-20190223-ready.csv
   target_files: temporary-steve-1-ready.df, temporary-steve-2-ready.df
         result: Complete job successfully
     start_time: 1550919973
       end_time: 1550919991
final_hit_count: 0
*************************** 2. row ***************************
             id: 7
         job_id: NoDefined_201902242138340001
      src_files: everyday-20190224-ready.csv
   target_files: temporary-steve-1-ready.df,temporary-steve-2-ready.df
         result: Complete job successfully
     start_time: 1551015514
       end_time: 1551015538
final_hit_count: 949

3. 为 colume & table 添加注释

create table t1 (
    c1 INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT "c1",
    c2 VARCHAR(100) COMMENT "c2",
    c3 VARCHAR(100) COMMENT "c3"
) COMMENT "this is a comment";
 
create index namedd on  t1(c2) comment "asfrads";

在创建表的时候,可以为 table、column、index 添加注释,注释的长度为 1024 个字符。table、column 的注释可以通过 show create table t1 来查看,index 注释可以直接通过 show index from t1 查看。

数据库 Index 的探究

Postgres 文档对于 Unique Indexes 的描述

Indexes can also be used to enforce uniqueness of a column's value, or the uniqueness of the combined values of more than one column.

索引可以强制某一列值必须唯一,或者多列联合起来唯一。创建语句为 CREATE UNIQUE INDEX name ON table (column [, ...]);。其中 NULL 不认为是相同的。Postgres 将自动对主键、唯一约束创建唯一索引。

Redis 基础

Redis 数据结构

Redis 不仅仅是个 K/V store,它支持多种数据结构。Redis Value 不限于简单的 String,可以在 Redis 中存储更复杂的数据类型。

1. String

2. List

String 元素的集合,本质上是一个链表,各元素按照插入顺序排列。

3. Set

唯一、无序的 String 元素集合

4. Sorted set

类似于 Set,但是 Sorted set 中每个 String 都关联着一个浮点数值,称作 Score,每个元素根据其 Score 做排序。

5. Hash

一个 hash table,Key & Value 均为 String

Redis Keys

  • 最大允许的 Key size 是 512MB
  • 空 string 也是被允许的
  • 最好通过 Schema 限制一下,例如 “object-type:id”

缓存中的工程问题

1. 缓存穿透

什么是缓存穿透?

假使是一个通过用户 ID 查找用户信息的场景,每次找寻到的信息都缓存在 Redis 中。但黑客试图一直请求一个负数用户 ID,导致每次请求都打到数据库,并发量大的话 DB 可能崩溃。

如何发现

统计缓存命中率,如果发现命中率很低,就可能有缓存穿透的问题。

解决方案

  1. 缓存空对象,记录下没有命中的 ID,并设置超时时间
  2. 数据实时性变化较低的话,可以使用 bloomfilter

2. 缓存雪崩

什么是缓存雪崩?

设置的缓存瞬时间全部失效,或者缓存层整体挂掉,请求直接到了 DB 来做查询,给 DB 造成极大负载,可能导致 DB 崩溃。

解决方案

  1. 针对于瞬时缓存失效,可以设置一定幅度的随机超时时间
  2. 对于缓存层挂掉
    1. 事前可以保证 Redis 的高可用
    2. 事中可以在 gateway 层限流。如果是微服务架构的话,可以及时 down 掉该服务,以免 DB 挂掉影响了其他服务。
    3. 事后,利用 Redis 持久化,重新启动加载数据,快速恢复缓存数据

命令行列表 之 连接符

多个命令同时输入的时候,常用到的连接符有 &&&||; ,本文依次记录一下各个连接符的作用。

\ 多行连接符

在每行命令后面加上 \ 即可将命令拆成多行。

& 异步操作符

如果一行命令以 & 符号结束,该命令将会异步执行,也就是说在后台执行,shell 工具将不会等待命令结束,直接返回 0 状态码。

; 顺序执行符

命令以 ; 为分隔符,将会被顺序执行,shell 工具将会等每个命令依次执行结束,最后返回的状态码是最后一条命令的退出状态码。

&& 与操作符

command1 && command2 command1 成功执行之后,才会执行 command2 。

|| 或操作符

command1 || command2 当 command1 返回码非 0 时,command2 才会执行。

linux hostname 修改

查看 hostname

hostnamectl status

修改 hostname

  1. 做 hostname 修改 hostnamectl set-hostname new_host_name
  2. 检查 /etc/hosts 看是否旧的 hostname 对应了本机 IP,及时做变更
  3. 重启机器 reboot

Go 内存分配

拜读 github 上一篇 神作,感觉自己领悟到了作者要传达的几乎所有信息,对于 Go 内存分配第一次有了全面的理解。但人的记忆毕竟有时效性,还是需要记下来,在自己脑子最热的时候记录下来,人家写的毕竟是人家的,我要记录下我的理解。

Go 进程在启动的时候会向操作系统申请一大块内存空间,然后把该空间划分为三部分:spansbitmaparena,arena 可以理解为heap,是真正存储数据对象的区域,bitmap 区域存储是辅助于 GC 的位图数据,spans 存储面向进程的找寻对象数据的数据。

Go 在内存管理上的最小单位是 span,每个 span 有其对应的唯一 Class,每个 Class 会标识一种对象大小的分块方式,比如 Class1 代表每个对象分配 8 byte。arena 区域的内存空间会按 page 划分,每个 page 是 8K。span 可说是按照某种 Class 来划分一个或多个 page 的结构体。

type mspan struct {
	next *mspan			//链表前向指针,用于将span链接起来
	prev *mspan			//链表前向指针,用于将span链接起来
	startAddr uintptr // 起始地址,也即所管理页的地址
	npages    uintptr // 管理的页数

	nelems uintptr // 块个数,也即有多少个块可供分配

	allocBits  *gcBits //分配位图,每一位代表一个块是否已分配

	allocCount  uint16     // 已分配块的个数
	spanclass   spanClass  // class表中的class ID

	elemsize    uintptr    // class表中的对象大小,也即块大小
}

span 中会记录自己使用了多少个 page,按照哪种 Class 来划分的块,span 之间通过链表形式串起来,所有它有前驱 & 后驱。

每个线程有自己的内存空间,记录在 mcache 这个对象中,这个对象记录了分配每种 Class 的 Index span。其结构体缩略如下:

type mcache struct {
	alloc [67*2]*mspan // 按class分组的mspan列表
}

alloc 长度为 Class 种类的二倍,每种 Class 都有包含指针的 Span 和不含 指针的 Span,这样的拆分是为了便于 GC ,对于没有包含指针的 Class,没必要去扫描。每个元素存储的是 Index Span,可以根据链表去找到分配该 Class 的所有 Span。

当每个线程上的 Span 使用完了后,会向 mcentral 对象去申请更多的 span,mcentral 也是对应 Class 的,每个 mcentral 只负责该 Class Span 的分配。mcentral 是全局性的,所以它存在互斥琐,其结构体简化如下:

type mcentral struct {
	lock      mutex     //互斥锁
	spanclass spanClass // span class ID
	nonempty  mSpanList // non-empty 指还有空闲块的span列表
	empty     mSpanList // 指没有空闲块的span列表

	nmalloc uint64      // 已累计分配的对象个数
}

它会保存未被分配的 span 列表和已被分配的 span 列表,mcache 需要更多 span 的时候会从空闲列表中拿一个,然后该 span 就挪入到了非空闲列表中去了。当然整个操作是需要加锁的,因为可能有多线程同时操作。

那么,mcentral 中的 Span 也分配完了怎么办呢?其会向 mheap 申请索要,mheap 是全局性的堆管理对象,其会向操作系统申请或者释放内存空间。mheap 管理所有的 mcentral,共有多少个呢?Class 类型数量的 2 倍,同 mcache 管理 span 一样,mcentral 也分为带指针需要扫描的和不需要扫描的,mheap 大致结构体如下:

type mheap struct {
	lock      mutex

	spans []*mspan

	bitmap        uintptr 	//指向bitmap首地址,bitmap是从高地址向低地址增长的

	arena_start uintptr		//指示arena区首地址
	arena_used  uintptr		//指示arena区已使用地址位置

	central [67*2]struct {
		mcentral mcentral
		pad      [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
	}
}

它是全局的,互斥琐少不了。它管理整个 arena 区域,所以包含 arena 的开始地址和使用长度,mheap 会管理每一个 mcentral,所以包含 67 * 2 个 mcentral,最后,还有最重要的基础单位 span,它要知道它所管理的每一个 span,所以包含了 span 指针数组。

总结

go 语言的内存分配包含了两级,负责整个进程内存管理的 mheap,其掌管着 arena 区域的内存分配。还有就是作为消费者的 mcache,其负责管理线程级别的内存管理。span 是内存管理的基础单位,mcrentral 负责在内存消费者线程与内存资源所有者进程中间做桥接,以提高内存的分配效率。

初识 Go GC

1. GC 回收算法

在学习 Go 内存分配 的时候有了解到 Span 的数据结构:

type mspan struct {
	next *mspan			//链表前向指针,用于将span链接起来
	prev *mspan			//链表前向指针,用于将span链接起来
	startAddr uintptr // 起始地址,也即所管理页的地址
	npages    uintptr // 管理的页数

	nelems uintptr // 块个数,也即有多少个块可供分配

	allocBits  *gcBits //分配位图,每一位代表一个块是否已分配
    gcmarkBits *gcBits  // GC 时用于标记块是否被引用
	allocCount  uint16     // 已分配块的个数
	spanclass   spanClass  // class表中的class ID

	elemsize    uintptr    // class表中的对象大小,也即块大小
}

其中 allocBits 指向一个分配位图,其每一位会标记每个块是否已被分配。

Go 采用 三色标记法 来作为 GC 算法。在执行 GC 时从根对象开始扫描,起初每个对象的初始状态为白色,代表对象未被标记,然后开始 GC 扫描,被引用的对象被标记为灰色,代表在标记队列中等待,接下来开始分析灰色对象,如果没有引用其他对象,则很快被标记为黑色,代表对象被标记不会在 本轮GC 中被回收,如果引用了其他对象,则将其引用的对象标记为灰色,并自身标记为黑色。最后没有被扫描到的对象依旧是白色,将会被 GC 回收。

标记完成后,allocBits 占用的内存将会被释放,其指针会指向 gcmarkBits,然后会重新分配一块空内存给 gcmarkbits。

2. GC 优化

设置写屏障(Write Barrier)

GC 执行的时候会控制住内存变化,不然这边刚标记为白色,那边就引用了它,最后被回收了,将造成严重的后果。避免这种情况使用的方法是 STW(Stop The World), 好像《来自星星的你》中都教授可以把时间凝固,然后做内存回收。但这样会影响程序运行效率,执行一会儿就要被冻结一次。

Go 又使用了 Write Barrier 来优化这个流程,在 GC 的某个阶段,写屏障被开启,指针传递时会被标记,GC 将不会在本周期内回收,下一次回收时再确定。GC 过程中新分配的内存不会被标记,用的不是写屏障技术,意为:GC 过程中分配的内存不会参与到本轮 GC。

辅助GC(Mutator Assist)

为了防止内存分配过快,在 GC 执行的时候,如果 goroutine 需要分配内存, 那么这个 goroutine 会帮助执行一些 GC 工作。

3. GC 的触发时机

内存分配的时候触发 GC

每次内存分配都会检查是否内存分配量已经达到了阀值,如果达到就要触发 GC。阀值为上次 GC 时内存分配的 2 倍,即每当分配的内存扩大一倍时触发 GC。

定期触发 GC

最长 2 分钟会强制触发一次 GC

手动触发

runtime.GC()

4. 如何优化 GC 性能

GC 的过程是扫描、标记、回收的过程,对象越多,GC性能越差。优化的话,我觉得是:

  1. 少分配对象,多使用指针引用,提高对象复用
  2. 使用大对象组合多个小对象

Go 闭包

什么是闭包

来自 Wikipedia,我也很认同的说法:闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。通俗的可以这样说:闭包 = 函数 + 环境变量。

通常,我们认知的函数是接受外部参数,然后执行计算。但闭包函数是函数中的函数,编译时为闭包函数分配了自由变量,自由变量和函数一同存在。

Golang 中的闭包使用

package main

func foo() func() int {
	a, b := 0, 1

	return func() int {
		res := a + b
		a, b = b, res
		return res
	}
}

func main() {
	f := foo()
	for i := 0; i < 10; i++ {
		println(f())
	}
}

这个例子中,f 就是那个引用了自有变量 a, b 的函数,即使 f 已经离开了创造它的 foo 函数,但自由变量 a, bf 同样存在不被回收。f 在每次被调用都会修改自由变量 a, b。在 Go 编译时,闭包会引起逃逸分析,本来看似会被分配到栈上的自由变量会转而分配到堆上。

同样的函数不同的自由变量

package main

import "fmt"

func kiko(i int) func() int {
	return func() int {
		i++
		return i
	}
}

func main() {
	f1 := kiko(0)
	println(f1()) // 1
	println(f1()) // 2

	f2 := kiko(0)

	println(f2()) // 1

	fmt.Printf("f1: %p \t f2: %p \n", f1, f2)   // f1: 0x109ad70 	 f2: 0x109ad70
}

每次被返回的内部函数都是同一个,我们来剖析一下 Go 闭包的实现,闭包底层的实现类似于这样一个结构体:

type Closure struct {
    F func()() 
    i *int
}

所以一个闭包包括函数 + 其引用的环境变量,如上函数打印的,每个闭包函数的地址都是一个,只是大家引用的环境变量不一样。

K8S 网络初步理解

Pod 的网络是怎样的?

Pod 是由多个 Container 组成,在同一 Pod 下各个 Container 共享网络 & 存储。每个 Pod 中会起一个 pause 的容器,专门负责网络转发,将流量根据端口号转向对应 Container。K8S 在创建一个 Pod 的时候会在 Pod 所在 Node 上创建一个虚拟网卡以 veth0 打头,该虚拟网卡供整个 Pod 使用,因此各 Container 之间可以通过 localhost 相互通信,并且对外暴露的端口不能重复。

Service 的网络是怎样的?

K8S 创建一个 Service 后,会分配一个 Cluster-IP,该 IP 在宿主机虚拟网卡中无法找到。该 Cluster-IP 是被 kube-proxy 通过 iptable 同步到 linux kenel 的 netfilter。请求 Service Cluster-IP 会被 netfilter 转发到对应的 Pod IP 。kubectl 负责检查 个 Pod 的健康状况,若发现一个 Pod 不可用会通过 api-server 通知 kube-proxy,kube-proxy 随即更新 iptable。

Go 高并发下全局自增的实现

1. 使用互斥琐 mutex

定义一个 struct,内嵌 sync.Mutex 使该结构体具备加锁 & 解锁的能力,该结构体中包含 Amount 统计变量,每次递增的时候都要执行加锁,计算结束之后再执行解锁。

type TotalAmount struct {
	Amount int64
	sync.Mutex
}

var t TotalAmount
var wg sync.WaitGroup

func main() {

	http.HandleFunc("/metuxPlus", func(w http.ResponseWriter, _ *http.Request) {
		t := &TotalAmount{}
		for i := 0; i < 99999999; i++ {
			wg.Add(1)
			go mutexPlus(t)
		}

		wg.Wait()
		println(t.Amount)

		w.Write([]byte("ok"))
	})

	http.Handle("/metrics", promhttp.Handler())
	http.ListenAndServe(":2112", nil)
}

func mutexPlus(t *TotalAmount) {
	t.Lock()
	t.Amount++
	t.Unlock()
	wg.Done()
}

2. 使用 atomic 包,执行原子操作

通过使用 golang atomic 包,可以对 integer 做原子操作。其背后使用的是汇编语言实现的。

var TotalAmount int64
var wg sync.WaitGroup

func main() {

	http.HandleFunc("/metuxPlus", func(w http.ResponseWriter, _ *http.Request) {
		for i := 0; i < 9999999; i++ {
			wg.Add(1)
			go atomicPlus()
		}

		wg.Wait()
		println(TotalAmount)
		w.Write([]byte("ok"))
	})

	http.Handle("/metrics", promhttp.Handler())
	http.ListenAndServe(":2112", nil)
}

func atomicPlus() {
	defer wg.Done()
	atomic.AddInt64(&TotalAmount, 1)
}

3. 使用 CSP 模型

New 一个长度为 1 的 int64 类型 Channel,每次流量过来都向该 Channel 传一个 1,有单一的 goroutine 来常驻监听该 Channel,对传来的值做消费。通过 Channel 保证了对全局变量的单线程操作。

var TotalAmount int64
var wg sync.WaitGroup

func main() {
	var TotalAmountChan = make(chan int64)

	go func() {
		println("Goble G")
		for {
			select {
			case v := <-TotalAmountChan:
				TotalAmount = TotalAmount + v
			}
		}
	}()

	http.HandleFunc("/metuxPlus", func(w http.ResponseWriter, _ *http.Request) {
		for i := 0; i < 99999; i++ {
			wg.Add(1)
			go func() {
				TotalAmountChan <- 1
				wg.Done()
			}()
		}

		wg.Wait()

		time.Sleep(time.Second * 2)
		println(TotalAmount)

		w.Write([]byte("ok"))
	})

	http.Handle("/metrics", promhttp.Handler())
	http.ListenAndServe(":2112", nil)
}

Go defer 关键字探析

Go 语言中有个十分有用的关键字 defer,平日里我们多数只是紧张的赶开发进度,可能很少去总结、梳理、沉淀,这篇文章就要来梳理一下几个问题:

  1. defer 到底是做什么的 ?
  2. defer 适用于哪些场景,能解决什么问题 ?
  3. defer 会产生哪些问题 ?

1. defer 关键字是用来做什么的 ?

最直接了当的回答:defer 关键字可以触发对函数或方法的 延迟调用,执行时机是所在函数结束前

来看一个例子:

package main

func main() {
	deferDemo()
}

func deferDemo() error {
	defer println("Call by Defer")

	println("Execute function")

	return SayHi()
}

func SayHi() error {
	println("Call SayHi function")
	return nil
}

通过这个例子我们来窥探一下 defer 调用的执行时机,这段代码的输出结果如下:

Execute function
Call SayHi function
Call by Defer

从输出结果可以明晰,字面上 defer 调用时机 所在函数结束前 可以更加清晰的阐述为:defer 指向的函数将会在其所在函数的所有操作(包括 return 语句中所触发的操作)执行完之后调用

写到这里就又会引出一个问题,即多个 defer 调用的执行顺序,多数文章中都会提到所以本文一笔带过,即:defer 调用所在的函数自上而下的执行,遇到 defer 关键字会将其指向的函数压入栈中,待函数所有操作执行完毕,再从栈中依据 后进先出 原则依次执行。

2. Defer 适用于哪些场景,能解决什么问题 ?

这是一个十分具体的问题,也很难回答的好,依据笔者的开发经验,给出如下我认为有价值的回答:

2.1 错误处理

脱离场景谈应用都是 toy play ,我们看这样一个场景:从数据源方和需求方分别拉取数据,然后将双方数据做碰撞(暂不关心细节),最后如果一切顺利就发邮件通知双方碰撞成功,中间出现任何错误也邮件通知双方碰撞失败。

由于 defer 调用 是在其所在函数所有操作执行完后执行的,我们就可以有这样的思路:声明命名返回值 err,数据碰撞过程中的错误全都赋值给这个变量。定义 defer 函数,在函数执行的最后来判断 err 变量,根据其是否为 nil 来决定发送成功或失败邮件。

代码大致如下:

func DoTask() (err error) {
	defer func() {
		if err != nil {
			SendSuccessfulMail()
		} else {
			SendFailedMail(err)
		}
	}()

	// pull data from each side
	err = pullData()

	// start to exchange data
	err = exchangeData()

	return
}

2.2 资源释放、解除锁定

return & panic 都会终止当前函数,触发延迟调用。所以可以在 defer 指向的函数中做一些 收尾工作。在一个有多处可能触发 return 的场景当中,我们无法判断函数可能在哪里中断,所以统一在 defer 调用中处理一些类似于:关闭文件流、关闭锁 等操作,是一个十分便捷和优雅的方案。

3. Defer 会产生哪些问题 ?

3.1 性能问题

代码的执行效率和开发效率往往是不可得兼的,让开发者觉得爽的语法糖,都是语言开发者背后做了许多工作的,defer 也不例外,其带来便捷的背后是包括注册、调用等操作,还有额外的缓存开销。

var m sync.Mutex

func call() {
	m.Lock()
	m.Unlock()
}

func deferCall() {
	m.Lock()
	defer m.Unlock()
}

func BenchmarkCall(b *testing.B) {
	for i := 0; i < b.N; i++ {
		call()
	}
}
func BenchmarkDeferCall(b *testing.B) {
	for i := 0; i < b.N; i++ {
		deferCall()
	}
}

benchmark 性能对比如下:

BenchmarkCall-4        	100000000	        17.7 ns/op
BenchmarkDeferCall-4   	20000000	        55.9 ns/op
PASS
ok  	better/color	2.979s

可知,即使是最简单的异步调用 unlock 都会有 3 倍的性能差。

3.2 参数传递可能带来的意外错误

在异步调用被注册的同时,参数值也被注册和缓存了起来。如小标题所言 可能意外,意即假使你完全弄明白了其中逻辑,是不会造成你对程序结果的误判的,来看如下例子:

func main() {
	var arr = []int{}

	defer func(val []int) {
		fmt.Printf("==> %v", val)  // ==> []
	}(arr)

	for i := 0; i < 10; i++ {
		arr = append(arr, i)
	}
}

在这个例子中,将会输出一个空 int array,原因是:

A deferred function's arguments are evaluated when the defer statement is evaluated.

defer 指令触发时,defer 函数的参数也就确定下来了。
届时 for 循环还未被执行,arr 还是空,所以输出结果为空。

解决的办法有两种,分别是 传递指针使用闭包。闭包本质上是函数与变量的绑定,这两个方法本质上是同一种方法。

func main() {
	var arr = []int{}

	fmt.Printf("Before Call >>>>>> %p \n", &arr)

	defer func(val *[]int) {
		fmt.Printf("==> %v \n", val)

		fmt.Printf("Pass Pointer >>>>>> %p \n", val)
	}(&arr)

	defer func() {
		fmt.Printf("==> %v \n", arr)

		fmt.Printf("Closure >>>>>> %p \n", &arr)
	}()

	for i := 0; i < 10; i++ {
		arr = append(arr, i)
	}

	fmt.Printf("Complete Call >>>>>> %p \n", &arr)
}

程序运行结果如下:

Before Call >>>>>> 0xc00008a020
Complete Call >>>>>> 0xc00008a020
==> [0 1 2 3 4 5 6 7 8 9]
Closure >>>>>> 0xc00008a020
==> &[0 1 2 3 4 5 6 7 8 9]
Pass Pointer >>>>>> 0xc00008a020

闭包可以在一个函数内使用函数外的环境变量。指针传递可以直接在延迟调用函数注册的时候复制指针的地址,调用被执行的时候通过指针的地址找到变量的地址,再通过变量的地址找到变量的值,从而输出正确结果。所以,向延迟调用函数传参的时候要谨慎注意。

总结

本文从 defer 关键字的使用方式、使用场景、可能的存在的问题三个方面来探析了一下 Go 语言这个语法糖。任何知识梳成条理,才能做到心中有数、游刃有余。路漫漫其修远兮,吾将上下而求索。

Go channel buffer

好久不碰 Channel,一小段代码让我有点疑惑,最后查出来是没有理解透彻 buffer size,在此记录下这个问题。

package main

func main() {
	var c = make(chan int)
	c <- 2309

	a := <-c

	println(a)
}

向一个 Channel 内传入一个值,然后再去获取这个值,很简单的示例,执行这段代码的结果却是:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/Users/Elegant/go/src/better/depDemo/main.go:5 +0x59
exit status 2

所有的 goroutine 都出于休眠态,死锁,为什么会这样?原来是 buffer size 没有理解到位。

什么是 buffer size ?

buffer size 是可以发送给 Channel 且不会造成发送阻塞的元素数量。默认的,通过 make(chan int) 创建的一个 Channel buffer size 为 0。这就意味着每一次发送消息都会造成阻塞,如果没有另一个 goroutine 在一直监听读取这个消息的话。如果 buffer size 设为 1,则发送第一个消息时不会造成阻塞,因为它进了缓冲区,第二个消息才会造成阻塞。

所以,如果代码要跑得通,可以修改下 buffer size:

package main

func main() {
	var c = make(chan int, 1)
	c <- 2309

	a := <-c

	println(a)
}

linux logrotate 工具的使用

logrotate 工具方便的实现了日志的轮换、压缩等功能,让管理员更加便捷的管理系统日志。

命令

logrotate <configfile>

配置文件

分别可以指定 global options & local options (local definitions override global ones, and later definitions override earlier ones) 。

# global options
compress

# local options
/var/log/messages {
    rotate 5
    weekly
    postrotate
        /usr/bin/killall -HUP syslogd
    endscript
}

主要配置项释义

1. rotate

日志文件在被删除或 mail 之前被轮换的次数,默认为 0 ,旧日志将直接被删除而不是轮换。

2. daily 、weekly、monthly、yearly

3. compress

旧日志将被 gzip 压缩

数据库是否需要外键约束?

去掉外键的坏处

  1. 会导致潜在的数据正确性问题,比如 A table 关联了 B table 的 ID,但该 ID 在 B table 中根本不存在
  2. 人们将不能通过 SQL 语句来知晓数据表结构关系,必须费大力气来阅读业务逻辑

去掉外键的好处

  1. 提升 Insert、Update、Delete 操作的性能,因为这些操作都需要去检查外键
  2. 跨数据库的能力增强,毕竟不同数据库对外键的支持是不一样的

总结

数据正确性数据库操作性能 之间还是要根据实际情况来做取舍,淘宝不建议使用外键,可能的一个因素是他们更关心效率,而对于普通项目,效率是否真的那么重要,已经要在外键取舍上抉择了?

grafana VS kibana

Grafana

  1. 针对 metrics 的分析
  2. 开箱即用的 alert

Kibana

  1. 针对 logs 的分析

表连接

Cross Join

SELECT e.fname, e.lname, d.name FROM employee e JOIN department d

交叉连接会产生笛卡尔积,即 {1, 2, 3} * {a, b, c} = 9 种情况

Inner Join

SELECT e.fname, e.lname, d.name 
FROM employee e INNER JOIN department d
ON e.dept_id = d.dept_id

如果没有指定连接类性,服务器会默认使用内连接,内连接在交叉连接的基础上增加了条件,所有不满足该条件的行都被结果集排除在外。

按照 SQL92 的标准,连接条件 & 过滤条件被分隔在 on & where 子句当中。虽然对于多数 DB 来讲连接条件、过滤条件放在一起也可以,但该用法是的在复杂语句中更清晰,SQL 语句更通用。

针对于表的连接顺序,对于 DB 来讲都一样。SQL 是一种非过程化的语言,也就是说只需要描述要获取的数据库对象,而如何最好的方式执行查询则由数据库服务器负责。

Outer Join

外连接分为左外连接(left outer join)和右外连接(right outer join)。关键词 left 指出连接左边的表决定结果集的行数,而右边的值负责提供与之匹配的列值。反之 right 同理。left & right 只是通知服务器那个表的数据可以不足。

SELECT a.account_id, a.cust_id, i.fname, i.lname
FROM account a LEFT OUTER JOIN individual i 
ON a.cust_id = i.cust_id

Linux 目录的权限位

Linux wrx 权限位作用在 file 上好理解,作用在 directory 上会是怎样一番作用?来梳理一下。

1. Read bit

读权限允许用户去读取文件夹中的文件(e.g. ls)。

2. Write bit

写权限允许用户在文件夹内创建、重命名、删除文件,调整目录的属性。

3. Execute bit

执行权限允许用户进入目录,访问其中的目录 & 文件。(e.g. cd)

crontab 笔记

易混淆的知识点

1. 每个参数的值域

  • min (0 ~ 59)
  • hour (0 ~ 23)
  • day of month (1 ~ 31)
  • month (1 ~ 12)
  • day of week (0 ~ 6) (Sunday=0)

2. 特殊的用法

  • * any value
  • 1,6,12 value list separator
  • 1-5 range of values
  • */5 step values

3. 需注意的

两个字段均可以设置 day (day of month & day of week), 如果两个参数都被设置了,则是一个累加的执行效果,均会在各自符合的条件下执行。

如何标记注释?

假使有很多 crontab 任务时,我们需要注释一下,以明确每个 job 的作用,crontab 使用 # 注释,例如:

# 每月清理一次 pm2 日志
0 0 1 * *  pm2 flush

我曾犯过的错误

需求场景:

在每天下午一点调用某个服务的 API

我的配置:

* 13 * * * curl http://127.0.0.1/some/api

错误分析:

* 是代表每一个符合区间域的数值,因为 API 只可能被调用一次,所以第一个参数必须给定一个值,不然就会每分钟调一次 API。所以当大的时间值给定之后,一定要三思一下小的时间值是否有必要给定。

案例实践

自动使用 letsencrypt 更新 HTTPS 证书

if you're setting up a cron or systemd job, we recommend running it twice per day (it won't do anything until your certificates are due for renewal or revoked, but running it regularly would give your site a chance of staying online in case a Let's Encrypt-initiated revocation happened for some reason). Please select a random minute within the hour for your renewal tasks.

为了保险起见,官方建议每天执行两次更新证书操作,此时我们可以这样写 Crontab Job:

0 2,5 * * * certbot renew

每天凌晨是用户活跃度最低的时候,我们可以设置任务在每天的 2:00 AM & 5:00 AM 执行更新证书操作。

使用 explain 来分析 SQL 的执行

explain select id from t_namespace where name = 'todo-app';

                                       QUERY PLAN
----------------------------------------------------------------------------------------
 Index Scan using t_namespace_name_key on t_namespace  (cost=0.14..8.16 rows=1 width=8)
   Index Cond: ((name)::text = 'todo-app'::text)
(2 rows)

可以通过 QUERY PLAN 来查看 SQL 的执行,(cost=0.14..8.16 rows=1 width=8) 表示 postgres 花费了 0.14 到 8.16 计算单位,共扫描了 1 行,width 代表返回结果的大小(单位为 byte)。

错误 & 异常的区别

在写代码的时候,我们经常会提到两个词 错误 & 异常,好多时候好多事情常常令人恼怒,但有则改之无则加勉,要感恩那些为你指出问题的人,感恩失败。

错误

错误分为两种:Semantic Errors & Syntactic Errors。前者语义错误指的是逻辑错误,这个错误只能由程序员自己来发现。后者为语法错误,这个会由编译器来检查发现。

异常

异常可以理解为一种 run-time 错误,本质上它也是一种错误。因为发生在运行时,所以编译器不能检查的到,并且它也不像语义错误那样,程序可以照常运行,只是运行结果错误。异常只能在运行时被发现,并且会阻碍程序的正常运行。

使用 cURL 做下载操作

wget 可以方便的做文件的下载,但是有时候机器上并没有预装 wget,这个时候就需要用 cURL 命令来下载文件了。

curl -O http://www.openss7.org/repos/tarballs/strx25-0.9.2.1.tar.bz2

注意这里使用 大写 O 参数,否则会把文件内容输出到 stdout,该操作即可下载远程文件,文件名和远程文件名一致。使用 –remote-name 参数也能达到同样的效果。

那么如果 URL 上没有标明文件名该怎么办呢?

curl -o taglist.zip http://www.vim.org/scripts/download_script.php?src_id=7701

这时需要 -o 小写 o 来指明文件名了,即可将对端输出流输入到 taglist.zip 文件中。

Postgres 常用命令

  1. \l 列举数据库
  2. \dt 列举表
  3. \d 查看表结构
  4. 查询某张表上的索引信息,select * from pg_indexes where tablename='t_subject';

Docker 数据持久化

Docker container 被移除的话,容器中的数据也将不复存在,Docker 提供了两种选择做数据持久化: volumes & bind mounts

volumes

volumes 的数据被存在 /var/lib/docker/volumes 中,非 docker 进程无法改动这个目录系统。docker 推荐的数据持久化方式。

  1. volumes 通过 docker volume create 创建,并通过 docker volume xxx 管理
  2. container 销毁后 volume 不会被清除,需要手动 docker volume rm || docker volume prune
  3. 同一个 volume 可以同时挂载多个 containers 中,同时被操作
  4. 有 volume drive 可以选择,可以实现远程挂盘等操作

bind mounts

Bind mounts 可以挂载任何的主机目录到容器,甚至重要的系统文件或目录。被挂载的目录允许被非Docker进程或者Docker container 操作。

  1. 存在安全风险,挂进来的任何文件都可被 container 修改
  2. 挂载时文件或者目录必须使用全路径(full path)
  3. Docker CLI 是无法直接管理 bind mount 的

Tips

  1. 如果空 volume 被挂到一个有文件的目录中,该目录中的文件会被 COPY 到 volume
  2. 如果已经存在文件的 volume || bind mount 被挂载到有文件的目录,容器内原有文件将会被遮挡。解除挂载后,container 中原先文件才能继续访问。

Linux 账号与用户组

用户的账号信息放置在 /etc/passwd,早起 UNIX 系统的密码也放置在该文件中,后来安全起见,将密码加密放在了 /etc/shadow 中了。

添加用户

useradd [-g 初始用户组] [-d 主文件夹绝对路径] [-s shell] 用户账号名

  1. -s nologin 使新建的用户默认无法登录,比如在创建 FTP 账户时,只允许该用户通过 FTP 登录
  2. 新创建的主文件夹内容是从 /etc/skel/* 中 COPY 过来的

删除用户

userdel [-r] username

  1. -r 连同用户主文件夹一起删掉

设置用户密码

passwd [-l 使密码失效 Lock] [-u 使密码恢复 Unlock] [--stdin 从前一个管道读取数据] 账号

  1. -S 列出密码相关参数

修改密码相关参数

chage [-l 列出密码详细参数] [-E 账号失效日] 账号名

修改用户信息

usermod [-l 新的用户名] [-d 新的主文件夹] 用户名

添加组

groupadd [-r 新建系统用户组] 用户组名

更改用户组名

groupmod [-n 新组名] 用户组名

删除用户组

groupdel 用户组名

chown 命令的使用

每个文件都关联着 owner & group ,chown 可以修改文件、链接、目录所关联的 owner & group。具体语法为: chown USER[:GROUP] FILEs

  1. chown USER:GROUP FILE 可同时修改 owner & group
  2. chown linuxize: file1 修改 owner 并把 linuxize 所在的组赋予文件
  3. chown :GROUP FILE 修改 group
  4. chown -R USER:GROUP DIRECTORY 递归修改

Go 语言 scheduler

1. runtime.GOMAXPROCS() 是做什么的?

The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously.

GOMAXPROCS 变量限制了可以并行执行用户层 Go 代码的操作系统线程数量。

2. G、M、P 是如何调度的?

When a new G is created or an existing G becomes runnable, it is pushed onto a list of runnable goroutines of current P. When P finishes executing G, it first tries to pop a G from own list of runnable goroutines; if the list is empty, P chooses a random victim (another P) and tries to steal a half of runnable goroutines from it.

上文来自 Dmitry Vyukov 在 《Scalable Go Scheduler Design Doc》 中的描述。翻译如下:当一个 G 被创建或者退出的 G 变得可运行的时候,他被加入到当前 P 的可运行 Goroutine 列表中。当 P 完成了 G 的执行,它把 G 从列表中推出;如果列表是空的,P 就随机选择其他的 P,偷一半的 G 过来。

405c3e45df3d4f72ad3e0877bd26230a

新加入的 P 概念,P 为 processor 的简写,可以理解为 CPU处理器,go scheduler 本质上是:把 goroutine 调度到操作系统线程上,操作系统线程运行在最多为 runtime.GOMAXPROCS() 个处理器上,scheduler 在三者之间寻找一个高效的运行方式。

struct P
{
    Lock;
    G *gfree; // freelist, moved from sched
    G *ghead; // runnable, moved from sched
    G *gtail;
    MCache *mcache; // moved from M
    FixAlloc *stackalloc; // moved from M
    uint64 ncgocall;
    GCStats gcstats;
    // etc
    ...
};

从结构体来看,至少可以看出 P 会存储 G,并标识哪些是可被执行的。

There is a P-specific local and a global goroutine queue. Each M should be assigned to a P. Ps may have no Ms if they are blocked or in a system call. At any time, there are at most GOMAXPROCS number of P. At any time, only one M can run per P. More Ms can be created by the scheduler if required.

摘自 rakyll 大神的博客,释意如下:整个调度中存在 P 的本地 G 队列,以及全局的 G 队列。每个 M 必须赋予 P 才能被执行。P 有时会没有 M ,当 M 都被阻塞的时候。任何时候,调度系统里有最多 GOMAXPROCS 个 P。任何时候,每个 P 只能运行一个 M。如果需要可以创建更多的 M。

02cfc34ab8404dd9b66c1defe3dae413

runtime 对 goroutine 的调度和操作系统对 进程 的调度不同,runtime 是一种合作调度,对于一个 goroutine 其会让他完全执行完毕。但 OS 的调度是基于 time slice 的调度,每个进程在时间片结束后,就会被调出,即使任务还未执行完毕。goroutine 只有在如下情况下才会被切换:

  • Channels send and receive operations, if those operations would block.
  • The Go statement, although there is no guarantee that the new Goroutine will be scheduled immediately.
  • Blocking syscalls like file and network operations.
  • After being stopped for a garbage collection cycle.

3. G 的调度顺序是怎样的?

runtime.schedule() {
    // only 1/61 of the time, check the global runnable queue for a G.
    // if not found, check the local queue.
    // if not found,
    //     try to steal from other Ps.
    //     if not, check the global runnable queue.
    //     if not found, poll network.
}

每一轮调度,都是找寻 G 然后执行它。会先搜索全局 G 列表,如果没有找到,就在本地列表找,本地列表也没有找到,就去其他 P 的本地列表窃取一半,还没找到再到全局列表去找。

Go 逃逸分析

逃逸分析(Escape Analysis) 由编译器在代码编译的时候完成,确定一个对象会分配在 Stack 还是 Heap。如果分配在栈上,函数运行结束后自动回收,如果分配在堆上,由 GC 负责回收。

1. 逃逸策略

  1. 如果对象没有外部引用,则分配在栈上
  2. 如果对象有被外部引用,则分配在堆上

没有外部引用的局部变量并不一定都分配在栈上,如果其占用内存过大,超过了栈的存储能力,也会被分配到堆上。

2. 逃逸场景

  1. 变量作为返回值被外部引用

    package main
    
    type Student struct {
        Name string
        Age  int
    }
    
    func StudentRegister(name string, age int) *Student {
        s := new(Student) //局部变量s逃逸到堆
    
        s.Name = name
        s.Age = age
    
        return s
    }
    
    func main() {
        StudentRegister("Jim", 18)
    }
  2. 闭包

    package main
    
    import "fmt"
    
    func Fibonacci() func() int {
        a, b := 0, 1
        return func() int {
            a, b = b, a+b
            return a
        }
    }
    
    func main() {
        f := Fibonacci()
    
        for i := 0; i < 10; i++ {
            fmt.Printf("Fibonacci: %d\n", f())
        }
    }
  3. 栈空间不足

    package main
    
    func Slice() {
        s := make([]int, 10000, 10000)
    
        for index, _ := range s {
            s[index] = index
        }
    }
    
    func main() {
        Slice()
    }
  4. 动态类型逃逸

    package main
    
    import "fmt"
    
    func main() {
        s := "Escape"
        fmt.Println(s)
    }

3. 总结

  1. 栈上分配内存更高效,也不需要 GC 来回收
  2. Value Copy 还是传指针?如果 Value 很小的话,还不如做值Copy,不然指针传递会产生逃逸现象,然后给 GC 造成压力。

SSH 日常使用

1. 远程主机密钥验证失败

➜  ~ ssh [email protected]
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ECDSA key sent by the remote host is
SHA256:ZGrbS4n6bY9F4Kf5uSY99KQhtvj/jvx9TWcbMOVPdX4.
Please contact your system administrator.
Add correct host key in /Users/Elegant/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/Elegant/.ssh/known_hosts:62
ECDSA host key for 117.50.39.88 has changed and you have requested strict checking.
Host key verification failed.

解决方案:

ssh-keygen -R "you server hostname or ip"

2. 生成 SSH Key

ssh-keygen

Linux 目录结果 & 文件权限

目录结构配置

  1. /usr (UNIX software resource): 与软件安装 / 执行有关
  2. /var (variable) 与系统运作过程有关
  3. /etc 系统主要的配置文件几乎都放置在这个目录,例如账号、密码

文件权限

dr-xr-x---.  5 root root  4.0K 8月  28 19:42 .
-rw-------   1 root root  7.1K 8月  28 19:42 .viminfo
-rw-r--r--   1 root root   42K 8月  28 19:42 .bash_history
-rw-r--r--   1 root root   196 8月  28 19:38 get_user_dir_size.sh

第一列第一个字符来标识文件类型:

d 目录
- 文件
l 连接文件
b 可供存储的接口设备
c 串行端口设备,如鼠标、键盘

第一列 2~9 个字符每三个为一组,依次代表:文件所有者权限同用户组权限其他非本用户组权限

文件权限 & 数字的对应关系

4 = r (Read)
2 = w (Write)
1 = x (eXecute)

Vim 使用笔记

光标移动

  • b 移动到上一个单词词首
  • e 移动到下一个单词词尾
  1. :tabnew + tab 打开指定文件在新标签页
  2. gt 切换标签页
  3. :set nu! 显示行号

Postgres Constraints

Check Constraints

实现对表某个列做检查

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CHECK (price > 0)
);

Not-Null Constraints

非空约束相当于 CHECK (column_name IS NOT NULL),但是直接设置 not-null 约束会更高效。

CREATE TABLE products (
    product_no integer NOT NULL,
    name text NOT NULL,
    price numeric NOT NULL CHECK (price > 0)
);

Unique Constraints(唯一约束)

唯一约束可以确保一列中的值在这张表的所有行中是惟一的

CREATE TABLE example (
    a integer,
    b integer,
    c integer,
    UNIQUE (a, c)
);
CREATE TABLE products (
    product_no integer UNIQUE,
    name text,
    price numeric
);

Primary Key

PRIMARY KEY 相当于 UNIQUE NOT NULL

Foreign Key

会检查该列中的值在另一张表中某列必须存在

CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    product_no integer REFERENCES products (product_no),
    quantity integer
);

go 是否是面向对象语言?

首先,先来解释下什么是 OOP(Object Oriented Programming)。OOP 核心在于定意一个 Object,可以理解为一个数据模型,然后围绕着它来做文章。 其需要有几个特性:

  1. 封装,函数内部公有属性可以被其他对象调用、修改,私有属性仅为内部使用
  2. 继承,存在子类的概念,子类可以继承父类的能力,减少重复代码的开发
  3. 多态,上下文的不同,对象可以有多种格式。比如 JS 中的 integer, 当和一个字符串相加的时候,其就变成的一个字符串

Go 官方团队对 Is Go an object-oriented language? 有表态过的,答案如下:

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

可以说是,也可以说不是。即使 Go 有类型、方法的实现,允许做面向对象风格的编程,但没有实现继承。Go 提供了 “interface” 这个概念,我们相信它是更易用更通用的。内嵌类型也是一种实现提供类似能力的方法,但它不是子类。此外,Go 中的方法比 CPP & Java 更通用,它可以定义在任意一种类型上,不局限于 Struct(Class)

没有了声明式的对象继承,使 Go 变得更加自由和轻量级。

MySQL & Postgres 选型参考

MySQL

  1. 仅作为网站后台存储,处理一些 transactions,没有复杂逻辑
  2. master-master & master-slave 都支持

Postgres

  1. 对于读写速度要求严苛,需要处理复杂计算
  2. 支持 master-slave 复制

new() vs make()

Allocation with make

make 只能用于对 map、slice、channel 做空间分配,原因是这三个类型在被使用前,底层数据结构必须被初始化。make(T) 返回 type T(not *T)

Allocation with new

new(T) 为类型 T 分配 zeroed storage,只分配空间,不初始化空间,并返回该类型的地址,类型为 *T

删除 Docker 中所有停掉的容器

Remove all stopped containers

docker container prune

Remove all containers

命令:

docker rm $(docker ps -a -q)

释义:

`-a` Show all containers (default shows just running)
`-q` Only display numeric IDs

Forward & Redirect 以及常见状态码

Forward

  1. 转发是 nginx 内部的事情,浏览器端毫无感知,地址栏还是显示原先的 URL

Redirect

  1. 浏览器将会去加载第二个 URL,
  2. 重定向将会更慢一些,因为浏览器要做两次请求

Forward or Redirect 的原因

  • 使链接更短,比如 t.tt 重定向到了 https://www.smartisan.com/
  • 当网站换域名的时候,旧的域名可以重定向到新的域名
  • 多域名指向同一个网站,比如 wikepedia.com、wikepedia.net、wikepedia.org

关于重定向状态码

3xx 都是重定向状态码,记录几个常用的:

HTTP Status Code Temporary / Permanent Cacheble Request Method Subsequent Request
301 Permanent Yes GET / POST may change
307 Temporary not by default may not change
308 Permanent by default may not change

其他:

  1. 304 Not Modified。请求的资源没有发生改变,此时无需重新传输资源。
  2. 301 永久重定向
  3. 302 临时重定向
  4. 401 对目标资源没有权限
  5. 504 Gateway Timeout 当网关或代理没有在指定时间段内获取到上游响应的时候,会报 504,因为网关或代理也是有请求超时时间的。
  6. 503 Service Unavailable 该状态码代表服务无法处理请求,服务不可用的原因可能是服务为了维护而停机,或者是请求过多服务器负载高。友好的做法是附带一个服务恢复时间给到用户。
  7. 502 Bad Gateway
    网关或者代理从上游服务接收到了无效的响应

Docker 网络

关于 docker0

docker 服务启动后会在 linux kernel 创建 docker0 虚拟网卡,其随机选择了一个地址和子网。默认所有的 Docker container 都连接到 docker0。连接到 docker0 的容器需要使用 iptables NAT 规则去与外部网络通信。

Docker Network

docker network create 可以自行创建 network interface,并且以 ID 的形式写在 ifconfig 中。新创建的网络与其他网络隔离。默认 docker run 使用的是 docker0 网络,同一网络下的 container 可以实现网络互通。但没有服务发现机制,需要使用 --link 把想要连接的 container 连接进来,这个时候,在 /etc/hosts 文件中就有对连接 container 的网络地址别名。

Docker 网络驱动类型

1. Bridge

默认创建私有网络的驱动。

2. None

将不使用网络接口,容器只能拥有 local loopback interface

3. Host

使用宿主机网络,这个时候端口不需要映射就能被外部访问。

Docker compose 启动背后做了什么?

When you run docker-compose up, the following happens:

  1. A network called myapp_default is created.
  2. A container is created using web’s configuration. It joins the network myapp_default under the name web.
  3. A container is created using db’s configuration. It joins the network myapp_default under the name db.

Container web & db 可以实现互通的原因是:

As of Docker 1.10, the docker daemon implements an embedded DNS server which provides built-in service discovery for any container created with a valid name or net-alias or aliased by link.

Docker 1.10 以后对于用户自定义的网络内置了 DNS 服务,可以通过 --name 定义 Container name, 然后通过 container name 来找到容器的 IP 。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.