#852 用户和客户

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 表,支持账号搜索就行。

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

2022-11-10

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

#850 为什么 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 交互的方式

#849 服务丧尸?为什么

2022-10-31

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

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

等后续有了进展再更新。

#848 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)
        }
    }
}

#847 CSDN 免登陆复制

2022-10-27

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

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

document.designMode = "on";

#846 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 网络配置。
这就不管了。

#845 日志平台的设计

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

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

#844 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)
}

#843 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