wang-kai / cherish-today Goto Github PK
View Code? Open in Web Editor NEWTo be a better programmer
License: MIT License
To be a better programmer
License: MIT License
正向代理服务让 LAN 客户机接入 WAN 以访问外网资源,正向代理服务器不支持外部对内部网络的访问请求。正向代理的目的如下:
反向代理服务器用来让外网客户端接入局域网中的站点以访问站点的资源。
一个 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;
}
之前源哥在开会上想要介绍一下我们的 gateway,认为这个基于反射技术的 gateway 是有技术创新性的,被我打断说这个技术没有创新性,2年前我就见到 Github 上有类似的。经过我 google 再三查找,并对所找到的方案做分析,认为源哥的实现方式还是最具通用性和稳妥的。
会后我找了 grpc-json-proxy,也能做类似的事情,这个项目是 inspired by grpc-json-example,grpc-json-example 作者在 2018 年 7月有一篇博客,专门介绍了他的实现方式。他发现:
这种实现有其优势,比如如此方式实现的 gateway 将不再需要后端提供 proto 文件,因为其不需要反射,后端服务改动接口,gateway 甚至不需要重启。因为其职责是加工处理下请求体、响应体,并且根据请求将其路由到对应的后端服务。
但这种方式有其很大的弊端:
grpc.CallContentSubtype(codec.JSON{}.Name())
特殊的 option这相当于使用了 gRPC 的通信方式,但没有使用 protobuf
这样的序列化载体。第一舍弃了 protobuf 这种精髓的技术,第二使得这个模块不通用,它提供的已经不再是通用的 gRPC 接口了。
Go 语言没有 enum
关键字的,通过使用 const
& iota
可以实现枚举的能力。本篇文章将探讨几个问题:
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 语言中没有 enum
类型,但我们可以通过 const
& iota
来实现。go 源码中有一段就是很好的示例代码。使用步骤如下:
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
的用法。
iota
代表了一个连续的整形常量,0,1,2,3 ...iota
将会被重置为 0 ,当再一次和 const
搭配使用的时候iota
所定义的值类型为 int
,它会在每次赋值给一个常量后自增// ext4 文件系统
mkfs.ext4 /dev/vdb
// xfs 文件系统
mkfs.xfs /dev/vdc
mount /dev/vdc /data
umount /dev/vdc
/etc/fstab
操作系统启动的时候会根据 /etc/fstab
配置来加载磁盘,为了避免重启后磁盘丢失,需修改 fstab
# fstab
/dev/vdb /data xfs defaults,nofail 0 0
两个主机,均有外网 IP、内网 IP。A 通过外网 IP 访问 B,B RemoteIP 显示的是 A 的外网 IP,A 通过 内网 IP 访问 B,B RemoteIP 显示的是 A 的内网 IP,为什么?
补充一个网络知识 NAT
,在上世纪 90 年代,可用的 IPv4 正面临枯竭的威胁。除了 IPv6 之外,一种最为重要的机制就是网络地址转换(NAT)。NAT 本质上是一种允许在互联网的不同地方重复使用相同 IP 地址集的机制,现在它已被大多数网络路由器所支持。
如下 IP 地址专供内网使用,不会出现在 Internet 中:
NAT
的工作原理就是重写通过路由器的数据包识别信息。这种情况发生在数据传输的两个方向上。NAT
会重写往一个方向传输的数据包的源 IP 地址,重写往另一个方向传输的数据包的目的 IP 地址。这允许传出的数据包的源 IP 地址变为 NAT 路由器中面向 Internet 的网络接口地址,而不是原始主机的接口地址。因此,在互联网上的主机看来,数据包来自具备全局路由 IP 的 NAT 路由器,而不是位于 NAT 内部的私有地址的主机。
IP报文在经过 router 的时候,router NAT 功能会修改数据包中的 IP & Port,把内网 IP 替换成可以在 Internet 上路由的 IP,并创建新端口,替换掉源端口。
等回包的时候再查看路由表,确定转发到哪个内网IP 的哪个端口,这就是 NAT(网络地址转换)。这就做到了一个外网 IP 支持了局域网上多个内网电脑的上网需求。
操作简介:
挂载目录的时候开启针对于 users, groups, and/or projects 选项,然后可以使用 xfs_quota
命令来限制或者查看 quota。
开启用户 quota
mount -o quota /dev/xvdb1 /xfs
开启针对于用户组的 quota
mount -o gquota /dev/xvdb1 /xfs
开启针对于项目的配额
mount -o prjquota /dev/xvdb1 /xfs
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'
The major difference between a container and an image is the top writable layer.
如文档中所言,Container 比 Image 多的,就是最上面一层可写层。Container 启动时会在镜像最顶层加一层 R/W layer。多个 Container 会共享基础镜像层,那么想修改一个文件怎么办呢?比如我想修改操作系统的 /etc/hosts
文件。
writable layer 是非常轻量级的,所有 container 对文件系统的改动都将存储于此。当要修改 /etc/hosts
文件时,存储驱动将触发 copy-on-write
操作,该操作大致有如下流程:
/etc/hosts
文件,被找到后将被添加到缓存,以提升未来的操作速度copy_up
操作,将 /etc/hosts
文件拷贝到可写层/etc/hosts
做改动,container 对于底层已经存在的 /etc/hosts
将是不可见的copy_up 操作会有明显的性能损耗,但该性能损耗仅在第一次文件修改的时候发生。因为可写层已经有了该文件。
Docker container 奉行 copy-on-write
的策略,写之前要经过查找、复制的流程,所以会大大降低写性能,所以不推荐在 Docker container 中做 I/O 密集的操作。
尽管如此,Docker 的 copy-on-write
策略有很大的优势:
:=
是我在学习 Go 的时候觉得挺怪的一个语言词法,因为我从 Java & Javascript 过来。但 :=
确实是 Go 引以为傲的的一个语法糖,甚至成为其标志性的语法。本文总结一下 :=
需要谨记的几个坑。
:=
相当于 声明并赋值,该操作必须放在函数内。
func f(i int) {
i := 4
println(i)
}
这里会报编译错误,因为传参的时候 i
已被声明,而函数中再次声明,所以编译报 no new variables on left side of :=
错误。
那么,如果使用 :=
同时操作多个变量,有的已被声明过,有的是新的变量,那么新的变量是声明并赋值,另外已在此作用域被声明过的变量就是做第二次赋值了。
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
, 它们是两个不同的变量。
读了文章 https://redis.io/topics/distlock
大致了解了一下基于 Redis 的分布式琐实现,当然只是读了文章,未尝自己实现,也没有看已实现的 library 代码。
获取琐 SET resource_name my_random_value NX PX 30000
。通过 Redis NX
& PX
确保同一资源名只能被一个客户端赋值,且该值有过期时间,避免了造成死锁。但该方案会有单点失败的问题。
多 Master 节点实现,而非一主多从。因为假使是一主多从,第一 Redis 的复制是异步的,抢琐的时候 slave 节点可能还没有同步到数据。第二,假使主节点挂掉,从节点变为主节点这种情况也是很复杂的。
Redlock
算法的流程:
大致就是 N 个 Master 节点,只要能获取 N/2 +1 个节点的琐,就算获取了琐。获取琐最好是多线程执行,并且设置超时时间,避免在与一个节点通讯时花费大量时间。比如过期时间为 10s,那么可以设置 timeout 时间为 40ms。
postgres 包含多种类型的索引:B-tree, Hash, GiST, SP-GiST, GIN and BRIN,B-tree 是默认的索引类型。
GIN 适合于包含多部分的值,例如 Array
show create table <table_name>;
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
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
查看。
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 不仅仅是个 K/V store,它支持多种数据结构。Redis Value 不限于简单的 String,可以在 Redis 中存储更复杂的数据类型。
String 元素的集合,本质上是一个链表,各元素按照插入顺序排列。
唯一、无序的 String 元素集合
类似于 Set,但是 Sorted set
中每个 String
都关联着一个浮点数值,称作 Score,每个元素根据其 Score 做排序。
一个 hash table,Key & Value 均为 String
什么是缓存穿透?
假使是一个通过用户 ID 查找用户信息的场景,每次找寻到的信息都缓存在 Redis 中。但黑客试图一直请求一个负数用户 ID,导致每次请求都打到数据库,并发量大的话 DB 可能崩溃。
如何发现
统计缓存命中率,如果发现命中率很低,就可能有缓存穿透的问题。
解决方案
什么是缓存雪崩?
设置的缓存瞬时间全部失效,或者缓存层整体挂掉,请求直接到了 DB 来做查询,给 DB 造成极大负载,可能导致 DB 崩溃。
解决方案
多个命令同时输入的时候,常用到的连接符有 &
,&&
,||
,;
,本文依次记录一下各个连接符的作用。
在每行命令后面加上 \
即可将命令拆成多行。
如果一行命令以 &
符号结束,该命令将会异步执行,也就是说在后台执行,shell
工具将不会等待命令结束,直接返回 0 状态码。
命令以 ;
为分隔符,将会被顺序执行,shell
工具将会等每个命令依次执行结束,最后返回的状态码是最后一条命令的退出状态码。
command1 && command2
command1 成功执行之后,才会执行 command2 。
command1 || command2
当 command1 返回码非 0 时,command2 才会执行。
tar -xf archive.tar.gz
tar zxvf backups.tgz
hostnamectl status
hostnamectl set-hostname new_host_name
/etc/hosts
看是否旧的 hostname 对应了本机 IP,及时做变更reboot
拜读 github 上一篇 神作,感觉自己领悟到了作者要传达的几乎所有信息,对于 Go 内存分配第一次有了全面的理解。但人的记忆毕竟有时效性,还是需要记下来,在自己脑子最热的时候记录下来,人家写的毕竟是人家的,我要记录下我的理解。
Go 进程在启动的时候会向操作系统申请一大块内存空间,然后把该空间划分为三部分:spans
、bitmap
、arena
,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 内存分配 的时候有了解到 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。
设置写屏障(Write Barrier)
GC 执行的时候会控制住内存变化,不然这边刚标记为白色,那边就引用了它,最后被回收了,将造成严重的后果。避免这种情况使用的方法是 STW(Stop The World), 好像《来自星星的你》中都教授可以把时间凝固,然后做内存回收。但这样会影响程序运行效率,执行一会儿就要被冻结一次。
Go 又使用了 Write Barrier 来优化这个流程,在 GC 的某个阶段,写屏障被开启,指针传递时会被标记,GC 将不会在本周期内回收,下一次回收时再确定。GC 过程中新分配的内存不会被标记,用的不是写屏障技术,意为:GC 过程中分配的内存不会参与到本轮 GC。
辅助GC(Mutator Assist)
为了防止内存分配过快,在 GC 执行的时候,如果 goroutine 需要分配内存, 那么这个 goroutine 会帮助执行一些 GC 工作。
内存分配的时候触发 GC
每次内存分配都会检查是否内存分配量已经达到了阀值,如果达到就要触发 GC。阀值为上次 GC 时内存分配的 2 倍,即每当分配的内存扩大一倍时触发 GC。
定期触发 GC
最长 2 分钟会强制触发一次 GC
手动触发
runtime.GC()
GC 的过程是扫描、标记、回收的过程,对象越多,GC性能越差。优化的话,我觉得是:
来自 Wikipedia,我也很认同的说法:闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。通俗的可以这样说:闭包 = 函数 + 环境变量。
通常,我们认知的函数是接受外部参数,然后执行计算。但闭包函数是函数中的函数,编译时为闭包函数分配了自由变量,自由变量和函数一同存在。
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, b
与 f
同样存在不被回收。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
}
所以一个闭包包括函数 + 其引用的环境变量,如上函数打印的,每个闭包函数的地址都是一个,只是大家引用的环境变量不一样。
Pod 是由多个 Container 组成,在同一 Pod 下各个 Container 共享网络 & 存储。每个 Pod 中会起一个 pause
的容器,专门负责网络转发,将流量根据端口号转向对应 Container。K8S 在创建一个 Pod 的时候会在 Pod 所在 Node 上创建一个虚拟网卡以 veth0
打头,该虚拟网卡供整个 Pod 使用,因此各 Container 之间可以通过 localhost 相互通信,并且对外暴露的端口不能重复。
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。
定义一个 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()
}
通过使用 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)
}
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
,平日里我们多数只是紧张的赶开发进度,可能很少去总结、梳理、沉淀,这篇文章就要来梳理一下几个问题:
defer
到底是做什么的 ?defer
适用于哪些场景,能解决什么问题 ?defer
会产生哪些问题 ?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
关键字会将其指向的函数压入栈中,待函数所有操作执行完毕,再从栈中依据 后进先出 原则依次执行。
这是一个十分具体的问题,也很难回答的好,依据笔者的开发经验,给出如下我认为有价值的回答:
脱离场景谈应用都是 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
}
return
& panic
都会终止当前函数,触发延迟调用。所以可以在 defer 指向的函数中做一些 收尾工作。在一个有多处可能触发 return
的场景当中,我们无法判断函数可能在哪里中断,所以统一在 defer 调用中处理一些类似于:关闭文件流、关闭锁 等操作,是一个十分便捷和优雅的方案。
代码的执行效率和开发效率往往是不可得兼的,让开发者觉得爽的语法糖,都是语言开发者背后做了许多工作的,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 倍的性能差。
在异步调用被注册的同时,参数值也被注册和缓存了起来。如小标题所言 可能
、意外
,意即假使你完全弄明白了其中逻辑,是不会造成你对程序结果的误判的,来看如下例子:
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 语言这个语法糖。任何知识梳成条理,才能做到心中有数、游刃有余。路漫漫其修远兮,吾将上下而求索。
好久不碰 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
是可以发送给 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)
}
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 压缩
数据正确性 和 数据库操作性能 之间还是要根据实际情况来做取舍,淘宝不建议使用外键,可能的一个因素是他们更关心效率,而对于普通项目,效率是否真的那么重要,已经要在外键取舍上抉择了?
Grafana
Kibana
SELECT e.fname, e.lname, d.name FROM employee e JOIN department d
交叉连接会产生笛卡尔积,即 {1, 2, 3} * {a, b, c} = 9
种情况
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 是一种非过程化的语言,也就是说只需要描述要获取的数据库对象,而如何最好的方式执行查询则由数据库服务器负责。
外连接分为左外连接(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 wrx
权限位作用在 file 上好理解,作用在 directory 上会是怎样一番作用?来梳理一下。
读权限允许用户去读取文件夹中的文件(e.g. ls
)。
写权限允许用户在文件夹内创建、重命名、删除文件,调整目录的属性。
执行权限允许用户进入目录,访问其中的目录 & 文件。(e.g. cd
)
Resource Type | Alias |
---|---|
ConfigMap | cm |
Ingress | ing |
deployment | deploy |
replicationcontrollers | rc |
Pod | po |
*
any value1,6,12
value list separator1-5
range of values*/5
step values两个字段均可以设置 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 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
错误,本质上它也是一种错误。因为发生在运行时,所以编译器不能检查的到,并且它也不像语义错误那样,程序可以照常运行,只是运行结果错误。异常只能在运行时被发现,并且会阻碍程序的正常运行。
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 文件中。
\l
列举数据库\dt
列举表\d
查看表结构select * from pg_indexes where tablename='t_subject';
Docker container 被移除的话,容器中的数据也将不复存在,Docker 提供了两种选择做数据持久化: volumes & bind mounts。
volumes 的数据被存在 /var/lib/docker/volumes
中,非 docker 进程无法改动这个目录系统。docker 推荐的数据持久化方式。
docker volume create
创建,并通过 docker volume xxx
管理docker volume rm
|| docker volume prune
Bind mounts 可以挂载任何的主机目录到容器,甚至重要的系统文件或目录。被挂载的目录允许被非Docker进程或者Docker container 操作。
Docker CLI
是无法直接管理 bind mount 的用户的账号信息放置在 /etc/passwd
,早起 UNIX 系统的密码也放置在该文件中,后来安全起见,将密码加密放在了 /etc/shadow
中了。
useradd [-g 初始用户组] [-d 主文件夹绝对路径] [-s shell] 用户账号名
-s nologin
使新建的用户默认无法登录,比如在创建 FTP 账户时,只允许该用户通过 FTP 登录/etc/skel/*
中 COPY 过来的userdel [-r] username
-r
连同用户主文件夹一起删掉passwd [-l 使密码失效 Lock] [-u 使密码恢复 Unlock] [--stdin 从前一个管道读取数据] 账号
-S
列出密码相关参数chage [-l 列出密码详细参数] [-E 账号失效日] 账号名
usermod [-l 新的用户名] [-d 新的主文件夹] 用户名
groupadd [-r 新建系统用户组] 用户组名
groupmod [-n 新组名] 用户组名
groupdel 用户组名
每个文件都关联着 owner & group ,chown 可以修改文件、链接、目录所关联的 owner & group。具体语法为: chown USER[:GROUP] FILEs
chown USER:GROUP FILE
可同时修改 owner & groupchown linuxize: file1
修改 owner 并把 linuxize 所在的组赋予文件chown :GROUP FILE
修改 groupchown -R USER:GROUP DIRECTORY
递归修改The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously.
GOMAXPROCS 变量限制了可以并行执行用户层 Go 代码的操作系统线程数量。
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 过来。
新加入的 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。
runtime 对 goroutine 的调度和操作系统对 进程 的调度不同,runtime 是一种合作调度,对于一个 goroutine 其会让他完全执行完毕。但 OS 的调度是基于 time slice 的调度,每个进程在时间片结束后,就会被调出,即使任务还未执行完毕。goroutine 只有在如下情况下才会被切换:
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 的本地列表窃取一半,还没找到再到全局列表去找。
逃逸分析(Escape Analysis) 由编译器在代码编译的时候完成,确定一个对象会分配在 Stack 还是 Heap。如果分配在栈上,函数运行结束后自动回收,如果分配在堆上,由 GC 负责回收。
没有外部引用的局部变量并不一定都分配在栈上,如果其占用内存过大,超过了栈的存储能力,也会被分配到堆上。
变量作为返回值被外部引用
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)
}
闭包
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())
}
}
栈空间不足
package main
func Slice() {
s := make([]int, 10000, 10000)
for index, _ := range s {
s[index] = index
}
}
func main() {
Slice()
}
动态类型逃逸
package main
import "fmt"
func main() {
s := "Escape"
fmt.Println(s)
}
➜ ~ 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"
ssh-keygen
目录结构配置
/usr
(UNIX software resource): 与软件安装 / 执行有关/var
(variable) 与系统运作过程有关/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)
b
移动到上一个单词词首e
移动到下一个单词词尾:tabnew + tab
打开指定文件在新标签页gt
切换标签页:set nu!
显示行号实现对表某个列做检查
CREATE TABLE products (
product_no integer,
name text,
price numeric CHECK (price > 0)
);
非空约束相当于 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)
);
唯一约束可以确保一列中的值在这张表的所有行中是惟一的
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
相当于 UNIQUE NOT NULL
会检查该列中的值在另一张表中某列必须存在
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
);
首先,先来解释下什么是 OOP(Object Oriented Programming)。OOP 核心在于定意一个 Object
,可以理解为一个数据模型,然后围绕着它来做文章。 其需要有几个特性:
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 变得更加自由和轻量级。
make
只能用于对 map、slice、channel
做空间分配,原因是这三个类型在被使用前,底层数据结构必须被初始化。make(T)
返回 type T(not *T)
。
new(T)
为类型 T 分配 zeroed storage
,只分配空间,不初始化空间,并返回该类型的地址,类型为 *T
。
docker container prune
命令:
docker rm $(docker ps -a -q)
释义:
`-a` Show all containers (default shows just running)
`-q` Only display numeric IDs
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 |
docker 服务启动后会在 linux kernel 创建 docker0 虚拟网卡,其随机选择了一个地址和子网。默认所有的 Docker container 都连接到 docker0。连接到 docker0 的容器需要使用 iptables NAT 规则去与外部网络通信。
docker network create
可以自行创建 network interface,并且以 ID 的形式写在 ifconfig
中。新创建的网络与其他网络隔离。默认 docker run
使用的是 docker0
网络,同一网络下的 container 可以实现网络互通。但没有服务发现机制,需要使用 --link
把想要连接的 container 连接进来,这个时候,在 /etc/hosts
文件中就有对连接 container 的网络地址别名。
1. Bridge
默认创建私有网络的驱动。
2. None
将不使用网络接口,容器只能拥有 local loopback interface
。
3. Host
使用宿主机网络,这个时候端口不需要映射就能被外部访问。
When you run docker-compose up, the following happens:
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 。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.