#843 Pipenv:PkgResourcesDeprecationWarning

2022-11-13
~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py:123: PkgResourcesDeprecationWarning: 1.1build1 is an invalid version and will not be supported in a future release
  warnings.warn(
~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py:123: PkgResourcesDeprecationWarning: 0.1.43ubuntu1 is an invalid version and will not be supported in a future release
  warnings.warn(
~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py:123: PkgResourcesDeprecationWarning: 2.9.0.Odd.Olm is an invalid version and will not be supported in a future release
  warnings.warn(

Pipenv 最近执行命令的时候会有这些提示,看这个意思是检测了一些 Python 包的版本不被支持。
用 traceback 排查之后,确认是在 Python PATH 下检查所有包的版本。
出问题的三个包是:

  • distro-info <LegacyVersion('1.1build1')> /usr/lib/python3/dist-packages
  • python-debian <LegacyVersion('0.1.43ubuntu1')> /usr/lib/python3/dist-packages
  • Shredder <LegacyVersion('2.9.0.Odd.Olm')> /usr/lib/python3/dist-packages

查找路径:

~/.local/lib/python3.10/site-packages/pipenv/patched
~/.local/lib/python3.10/site-packages/pipenv/vendor
~/Projects/Mine/staticize
/usr/lib/python310.zip
/usr/lib/python3.10
/usr/lib/python3.10/lib-dynload
~/.local/lib/python3.10/site-packages
~/.local/lib/python3.10/site-packages/mackup-0.8.33-py3.10.egg
/usr/local/lib/python3.10/dist-packages
/usr/lib/python3/dist-packages

相关代码:

File "~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py", line 997, in __init__
  self.scan(search_path)
File "~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py", line 1030, in scan
  self.add(dist)
File "~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py", line 1050, in add
  dists.sort(key=operator.attrgetter('hashcmp'), reverse=True)
File "~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py", line 2623, in hashcmp
  self.parsed_version,
File "~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py", line 2671, in parsed_version
  self._parsed_version = parse_version(self.version)
File "~/.local/lib/python3.10/site-packages/pkg_resources/__init__.py", line 122, in parse_version
  print(''.join(traceback.format_stack()))

#842 HTTPS 知识点

2022-11-11

HTTPS = HTTP + TLS
原来是 TCP -> HTTP
现在是 TCP -> TLS -> HTTP

提升了安全性的同时,降低了一些性能。

更重要的是,互联网基础设施层面开始淘汰 HTTP:

  1. 包括谷歌在内的浏览器厂商将使用 HTTP 访问的网站标记为不安全网站,Google 还会降低 HTTP 网站的排名。
  2. 其他平台,比如苹果 App Store,微信小程序等,都要求使用 HTTPS 协议。

1、怎么提升安全性

  1. 加密数据传输: TLS 数据加密,难以被窃听和解密。即使攻击者能够截获传输的数据包,他们也不能轻易读取其中的内容。
  2. 身份验证: 受信任的第三方机构颁发的 TLS 证书,可以用来验证服务器身份。
  3. 完整性验证: 使用消息摘要算法(如 SHA-256)来验证数据的完整性,防止数据被篡改(中间人攻击)。

2、降低了多少性能

相关通信过程在 2021/01/08,了解 HTTPS 背后的原理 有描述。

损耗主要在哪些环节呢?

  1. 握手
  2. 加密、解密
  3. 数据传输(加密之后数据增大)
  4. 证书验证 - 优化:浏览器缓存证书

3、如何优化

参考 HTTPS 的资源消耗,针对消耗点进行优化。

4、工具

5、清单

在 2012 年的 RFC 6797 中,HTTP 严格传输安全被定义为网络安全标准。 创建这个标准的主要目的,是为了。

  • 证书
    • OCSP(在线证书状态协议):检查证书有效期,确保没有被吊销
  • TLS 协议版本
  • 加密套件
  • HSTS(HTTP 严格传输安全)
    RFC 6797。
    作用是避免用户遭受使用 SSL stripping(剥离,HTTP 降级攻击) 的 中间人攻击(man-in-The-middle,MITM)

    add_header Strict-Transport-Security "max-age=31536000";
    
  • HTTP/2 支持

  • Apple ATS(App Transport Security)
    > 苹果 ATS 证书的选择及配置
    > 自 2017 年 01 月 01 日起,根据苹果公司要求,所有 iOS 应用必须使用 ATS(App Transport Security),即 iOS 应用内的连接必须使用安全的 HTTPS 连接。

#841 用户和客户

2022-11-11

先说一句,文章链接中的单词不小心写错了,customer,不要在意这些细节。

我司曾经强调过在内外交流中区分客户和用户,不要混用(主要是统一表达方式)。今天突然想到这个问题,特意来仔细琢磨一下。

我司的定义

我记得大概是说:

  • 客户:购买我们产品的人
  • 用户:客户使用我们产品去服务的对象,也就是客户的客户

我们提供的是消息触达服务(邮件和短信),落到产品中,基础功能可以免费使用(定量),增值功能需要收费。这是背景。
在这个过程中,客户是我们产品的使用者,客户使用我们的产品给他们的客户发送消息。
客户的客户,也就是消息的接收方,就称之为用户。

PS:客户又分成付费客户和免费客户。

这个和原义不大相符,作为内部规范,怎么定义都行,上面怎么说就怎么来。

我认为

  1. 客户和用户

    1. 客户,Customer,这描述的是商务关系,认为是购买产品的人,没毛病。
    2. 用户,User,应该是使用我们产品的人,也就是我们的服务对象。

    我在网上找了一遍,大概都是这个意思。和我对这个两个词语义上的理解是吻合的。

  2. 客户内部的角色

    1. 决策人(KP)
    2. 购买人/付款人
    3. 使用人

    他们应该整体作为一个客户,不能区分开来,认为是决策人,或者购买人是客户,使用人是用户。

    比如爸爸为孩子挑选了一款平板电脑,妈妈付钱,孩子使用。在平板电脑公司来看,应该认为他们是一个整体,然后找到 KP 做营销,告知这款平板多么利于学习,价格多么合适,还有最后一天的节日优惠。然后为了企业的品牌形象与二次营销,应该提升产品的质量和使用体验(持续更新的学习资料 + 游戏性能)。

  3. 客户使用我们的产品去通知也好,去营销也好,这些消息的接收方,应该就只是叫做收信方。


通过上面的定义,我们应该很容易理解下面几个观点:

  1. 仅根据是不是有商务关系(付钱)判断是不是客户。
  2. 用户分成免费用户、付费用户。
    1. 免费用户一定不是客户。
      免费用户:仅在免费额度范围内使用基础服务、部分产品提供的免费试用。
    2. 付费用户不一定是客户,因为可能是客户购买之后提供给其他人使用的。
      1. 我以前做过的一款产品就是客户购买之后,客户的客户使用。

因为日常生活中,很多时候,客户(购买者)往往就是用户(使用者),我们才容易搞混。
在一些业务场景下,我们假定购买者就是使用者,那么付费用户也可以说成是付费客户。
免费用户不能说免费客户(因为没有商务关系)。

产品和服务

在写这篇文章的时候,我又想起另一个概念来了,我们是提供产品,还是提供服务?

如果没记错,在内部会议上讨论过,老板阐述了自己的观点。
当时的讨论内容我忘了,我这里就说说我现在的想法。

比如,我买周黑鸭送给了客户,客户转手把周黑鸭送给朋友吃了。
周黑鸭的客户是我,没有问题。周黑鸭的用户是谁?

如果按照提供产品的观点,最后产品被老板的朋友吃了,那么老板的朋友是用户。
周黑鸭应该为我提供体面又具有性价比的产品,同时为最后吃到鸭脖子的人提供美味(产品体验)。

如果按照提供服务的理念,周黑鸭公司提供的是礼品服务,那么是我在使用这个服务,所以我既是客户,又是用户。
周黑鸭应该为我提供体面又具有性价比的礼品服务,同时为我提供最好的送礼效果(产品体验),就是让我的客户收到礼物之后觉得满意,然后我和客户的友好度 +1。

这个例子可能不太恰当,但是我想表达的意思都说清楚了。

再补充一个,账户与账号

账户 Account 用户 User
账户名 Account Name 用户名 User Name
账户 ID(账号) Account ID 用户 ID User ID

账户是用户在系统内的身份,理论上一个用户可能有多个账户。

  1. 大多数情况,一个用户一个账户。
  2. 更重要的是,用户是服务对象,落实到管理系统中,用户就是账户。
  3. 就算一个用户使用多个身份信息(手机号)注册了多个账户,也没人会特意去调查、区分它们。

所以用户和账户也经常混用。

如果一个用户有多个账户的时候还是会发生一些沟通上的不便,严谨一点,内部对齐客户、用户、账户这几个概念,在不同场景下根据上下文区分使用,还是有一些必要性的。


就系统开发来说:

  • 用户表:user_id, username, password, customer_id
  • 客户表:customer_id, customer_name

  • 新用户注册的时候,自动同步到客户管理系统 CRM 中(潜在客户)。

  • 到了商务对接阶段,也就是成为了付费客户之后,如果发现客户创建了几个账号,可以在 CRM 中将这几个账号关联到一起。
  • 后台就客户信息进行管理,也就是基于 customer 表,支持账号搜索就行。

#840 转载:Linux/UNIX 编程如何保证文件落盘

2022-11-10

我们编写程序 write 数据到文件中时,其实数据不会立马写入磁盘,而是会经过层层缓存。每层缓存都有自己的刷新时机,每层缓存都刷新后才会写入磁盘。这些缓存的存在是为了加速读写操作,因为如果每次读写都对应真实磁盘操作,那么读写的效率会大大降低。带来的坏处是如果期间发生掉电或者别的故障,还未写入磁盘的数据就丢失了。对于数据安全敏感的应用,比如数据库,比如交易程序,这是无法忍受的。所以操作系统提供了保证文件落盘的机制。我们来看下这些机制的原理和使用。

#839 为什么 C/C++ 不可替代

2022-10-31

英文原文:https://levelup.gitconnected.com/why-modern-alternative-languages-never-replace-c-c-cbf0afc5f1dc
中文翻译:https://mp.weixin.qq.com/s/tr69w96gOO7ia81ttilgVA

总结一下:

  1. C/C 是优秀的系统级编程语言,无数基础设施是用 C/C 开发的
  2. C/C++ 可以完全控制机器,开发者拥有最大的灵活性
  3. C/C++ 性能非常好
  4. C/C++ 学术友好,简单,高效,直接(没有封装得很抽象)
  5. 所有操作系统提供 C API,其他语言也都提供了和 C 交互的方式

#838 服务丧尸?为什么

2022-10-31

发现一个问题,暂时没有任何思路:

一个线上执行了 66 天的服务,突然在前天凌晨 1:30 没有日志输出了,从外网连接不上,内网可以连上。

等后续有了进展再更新。

#837 Go DNS 解析

2022-10-30

A 记录

package main

import (
    "fmt"
    "net"
)

func main() {
    // 方法 1
    // func ResolveIPAddr(network, address string) (*IPAddr, error)
    ipAddr, err := net.ResolveIPAddr("ip", "www.google.com")
    if err != nil {
        fmt.Println("解析IP地址失败:", err)

    } else {
        fmt.Println("IP地址是:", ipAddr.IP)
    }

    // 方法 2
    // func LookupMX(name string) ([]*MX, error)
    // func LookupTXT(name string) ([]string, error)
    // func LookupIP(host string) ([]IP, error)
    // func LookupHost(host string) ([]string, error) // only IPv4
    ips, err := net.LookupHost("www.google.com")
    if err != nil {
        fmt.Println("解析主机名失败:", err)
    } else {
        for _, ip := range ips {
            fmt.Println("IP地址是:", ip)
        }
    }
}

MX 记录

package main

import (
    "fmt"
    "net"
)

func main() {
    records, err := net.LookupMX("qq.com")
    if err != nil {
        fmt.Println("解析MX记录失败:", err)
    } else {
        fmt.Printf("%#v", records)
    }
}

指定 DNS 服务器

func queryMX(dns string, domain string) ([]*net.MX, error) {
    resolver := &net.Resolver{
        PreferGo: true,
        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, "udp", dns)
        },
    }
    return resolver.LookupMX(context.Background(), domain)
}

完整版本

package main

import (
    "context"
    "fmt"
    "net"
    "strings"
    "time"
)

var dnsServers = []string{
    "223.5.5.5:53",
    "114.114.114.114:53",
    "8.8.8.8:53",
}

type IPType int

const (
    NotIP IPType = iota
    IPv4
    IPv6
)

const (
    DNSLookupTimeout = 500 * time.Millisecond // time.Duration
)

func GetIPType(host string) IPType {
    ip := net.ParseIP(host)
    if ip == nil {
        return NotIP
    }
    if ip.To4() != nil {
        return IPv4
    }
    if ip.To16() != nil {
        return IPv6
    }
    return NotIP
}

type Resolver struct {
    DNS      string
    Resolver *net.Resolver
}

func NewResolver(dns string) Resolver {
    resolver := Resolver{}
    resolver.DNS = dns
    resolver.Resolver = &net.Resolver{
        PreferGo: true,
        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, "udp", dns)
        },
    }
    return resolver
}

func (r Resolver) QueryA(domain string) ([]string, error) {
    return r.Resolver.LookupHost(context.Background(), domain)
}

func (r Resolver) QueryMX(domain string) ([]*net.MX, error) {
    ctx, cancel := context.WithTimeout(context.Background(), DNSLookupTimeout)
    defer cancel()
    return r.Resolver.LookupMX(ctx, domain)
}

func (r Resolver) Query(domain string) ([]*net.MX, error) {
    addrs, err := r.QueryMX(domain)
    if err != nil {
        return nil, err
    }
    var result []*net.MX
    for _, addr := range addrs {
        // hostname := strings.TrimRight(addr.Host, ".")
        ipType := GetIPType(addr.Host)
        fmt.Printf("%s: %#v\n", addr.Host, ipType)
        if ipType == NotIP {
            ips, err := r.QueryA(addr.Host)
            if err != nil {
                continue
            }
            for _, ip := range ips {
                result = append(result, &net.MX{Host: ip, Pref: addr.Pref})
            }
        }
    }
    return result, nil
}

var resolvers map[string]Resolver

func Query(domain string) ([]*net.MX, error) {
    var addrs []*net.MX
    var err error
    for _, dns := range dnsServers {
        var r Resolver
        if r, ok := resolvers[dns]; !ok {
            r = NewResolver(dns)
            resolvers[dns] = r
        }
        addrs, err = r.Query(domain)
        if err == nil {
            break
        }
        fmt.Printf("Error: %s: %s: %#v\n", dns, domain, err)
    }
    return addrs, err
}

func QueryAsync(domain string, ch chan<- []*net.MX, errCh chan<- error) {
    addrs, err := Query(domain)
    if err != nil {
        errCh <- err
        return
    }
    ch <- addrs
}

func init() {
    resolvers = make(map[string]Resolver)
}

func main() {
    {
        domains := []string{"qq.com", "gmail.com", "google.com"}
        for _, domain := range domains {
            fmt.Println(strings.Repeat("=", 100))
            addrs, err := Query(domain)
            if err != nil {
                fmt.Printf("Error: %#v\n", err)
            } else {
                for _, addr := range addrs {
                    fmt.Printf("%#v\n", addr)
                }
            }
        }
    }

    {
        fmt.Println(strings.Repeat("=", 100))
        ch := make(chan []*net.MX)
        errCh := make(chan error)
        go QueryAsync("google.com", ch, errCh)
        select {
        case addrs := <-ch:
            fmt.Println("MX Records:")
            for _, addr := range addrs {
                fmt.Printf("%#v\n", addr)
            }
        case err := <-errCh:
            fmt.Println("Error:", err)
        }
    }
}

#836 CSDN 免登陆复制

2022-10-27

CSDN 会拦截复制,提示需要登录。

近日在网上学到一招,在控制台输入一行代码就好了:

document.designMode = "on";

#835 ping github.com 得到 127.0.0.1 问题

2022-10-27
git clone git@github.com:tornado/tornado
Cloning into 'tornado'...
ssh: connect to host github.com port 22: Connection refused
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
ping github.com

正在 Ping github.com [::1] 具有 32 字节的数据:
来自 ::1 的回复: 时间<1ms
来自 ::1 的回复: 时间<1ms
来自 ::1 的回复: 时间<1ms

::1 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 0ms,最长 = 0ms,平均 = 0ms
Control-C
ping -4 github.com

正在 Ping github.com [127.0.0.1] 具有 32 字节的数据:
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64

127.0.0.1 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 0ms,最长 = 0ms,平均 = 0ms
Control-C
nslookup github.com
Server:         192.168.31.1
Address:        192.168.31.1#53

Non-authoritative answer:
Name:   github.com
Address: 127.0.0.1
Name:   github.com
Address: ::1

配置 DNS 为 AliDNS 223.5.5.5233.6.6.6

nslookup github.com
Server:         223.5.5.5
Address:        223.5.5.5#53

Non-authoritative answer:
Name:   github.com
Address: 20.205.243.166

问题依旧。

ping 20.205.243.166

正在 Ping 20.205.243.166 具有 32 字节的数据:
请求超时。
请求超时。
请求超时。

20.205.243.166 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 0,丢失 = 3 (100% 丢失),
Control-C
ping baidu.com

正在 Ping baidu.com [39.156.66.10] 具有 32 字节的数据:
来自 39.156.66.10 的回复: 字节=32 时间=23ms TTL=52
来自 39.156.66.10 的回复: 字节=32 时间=24ms TTL=52
来自 39.156.66.10 的回复: 字节=32 时间=23ms TTL=52

39.156.66.10 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 23ms,最长 = 24ms,平均 = 23ms
Control-C

总结

  1. hosts 没有特殊配置
  2. 改成可用 DNS,问题没有修复
  3. ipconfig /flushdns 也不管用
  4. 重启也没有好

那就奇怪了,这个 ping 里面的本地回环地址是哪里来的呢?

最后,

ipconfig /displaydns 可以看到,还是用的回环地址
ipconfig /all 发现还有一个 DNSv6 配置。。。

DNSv6 设置成 AliDNS 的 IPv6 地址:2400:3200::12400:3200:baba::1 再试,

ping github.com

正在 Ping github.com [20.205.243.166] 具有 32 字节的数据:
请求超时。
请求超时。
请求超时。
请求超时。

20.205.243.166 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 0,丢失 = 4 (100% 丢失),

终于好了。😂

事情的起源是 git clone 失败,最后 git clone 可以了。
ping 不通,可能是网络问题,也可能是 github 网络配置。
这就不管了。

#834 日志平台的设计

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 同步到

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