December 15, 2021
自从微服务大行其道,容器化和k8s编排一统天下之后,“可观测性” 便被提出来。这个概念是指,对于应用或者容器的运行状况的掌控程度,其中分为了三个模块:Metrics
、Tracing
、Logging
。Metrics 指应用采集的指标;Tracing 指应用的追踪;Logging 指应用的日志。
日志自不用多说,这是最原始的调试和数据采集能力。Metrics 比较火的方案就是 Prometheus + Grafana,思路就是通过应用内埋入SDK,选择 Pull 或者 Push 的方式将数据收集到 prometheus 中,然后通过 Grafana 实现可视化,当然这不是本文的重点就此略过。
Tracing 也并不是可观测性提出后才诞生的概念,在微服务化的进程中就已经有Google的Dapper落地实践,并慢慢形成 OpenTracing 规范,这一规范又被多家第三方框架所支持,如 Jaeger、Zipkin 等。OpenTelemetry 就是结合了 OpenTracing + OpenCensus 规范,约定并提供完成的可观测性套件,只是目前(2021-12-15)稳定下来的只有 Tracing 这一部分而已。对 OpenTelemetry 发展历史感兴趣的可以自行了解。
效果预览
#
链路总览,包含了前端页面的生命周期 + 整个了链路采集到的Span聚合。
前端页面指标采集概览,包含了该页面生命周期内的动作和日志等。
服务端链路细节,包含了服务端链路采集的标签和日志(事件)等信息。
propagation兼容jaeger效果,保证jaeger侧链路完整,使用一致的 traceId检索。因为服务侧 sentry 是渐进更新的,因此没有接入的应用并不会展示在sentry侧, 等到完全更新后就会完整。
背景
#
目前运行中的链路追踪组件是采用 opentracing + jaeger 实现,这套方案唯二的不足就是:
前端采用 sentry 来采集前端页面数据(APP + WEB 都支持很好),因此才有了这么一个 前后端链路打通的需求。
...
September 22, 2020
背景
#
第一次,线上遇到大量接口RT超过10s触发了系统告警,运维反馈k8s集群无异常,负载无明显上升。将报警接口相关的服务重启一番后发现并无改善。但是开发人员使用链路追踪系统发现,比较慢的请求总是某个gRPC服务中的几个POD导致,由其他POD处理的请求并不会出现超时告警。
第二次,同样遇到接口RT超过阈值触发告警,从k8s中查到某个gRPC服务(关键服务)重启次数异常,查看重启原因时发现是OOM Killed
,OOM killed
并不是负载不均衡直接导致的,但是也有一定的关系,这个后面再说。前两次由于监控不够完善(于我而言,运维的很多面板都没有权限,没办法排查)。期间利用pprof分析了该服务内存泄漏点,并修复上线观察。经过第二次问题并解决之后,线上超时告警恢复正常水平,但是该 deployment 下的几个POD占用资源(Mem / CPU / Network-IO),差距甚大。
第二张图是运维第一次发现该服务OOM killed 之后调整了内存上限从 512MB => 1G,然而只是让它死得慢一点而已。
从上面两张图能够石锤的是该服务一定存在内存泄漏。Go项目内存占用的分析,我总结了如下的排查步骤:
1. 代码泄漏(pprof)(可能原因 goroutine泄漏;闭包)
2. Go Runtime + Linux 内核(RSS虚高导致OOM)https://github.com/golang/go/issues/23687
3. 采集指标不正常(container_memory_working_set_bytes)
2,3 是基于第1点能基本排除代码问题的后续步骤。
解决和排查手段:
1. pprof 通过heap + goroutine 是否异常,来定位泄漏点
运行`go tool pprof`命令时加上--nodefration=0.05参数,表示如果调用的子函数使用的CPU、memory不超过 5%,就忽略它。
2. 确认go版本和内核版本,确认是否开启了MADV_FREE,导致RSS下降不及时(1.12+ 和 linux内核版本大于 4.5)。
3. RSS + Cache 内存检查
> Cache 过大的原因 https://www.cnblogs.com/zh94/p/11922714.html
// IO密集:手动释放或者定期重启
查看服务器内存使用情况: `free -g`
查看进程内存情况: `pidstat -rI -p 13744`
查看进程打开的文件: `lsof -p 13744`
查看容器内的PID: `docker inspect --format "{{ .State.Pid}}" 6e7efbb80a9d`
查看进程树,找到目标: `pstree -p 13744`
参考:https://eddycjy.com/posts/why-container-memory-exceed/
通过上述步骤,我发现了该POD被OOM killed
还有另一个元凶就是,日志文件占用。这里就不过多的详述了,搜索方向是 “一个运行中程序在内存中如何组织 + Cache内存是由哪些部分构成的”。这部分要达到的目标是:一个程序运行起来它为什么占用了这么些内存,而不是更多或者更少。
...
August 12, 2020
背景
#
在 github.com/yeqown/goreportcard 项目中我改造了 goreportcard
。
后续为了方便部署,我准备将其打包成为docker镜像并上传到 DockerHub
。期间遇到了下面的问题,并一一解决,这里做一个记录帮助以后遇到类似的问题可以快速解决。
初期的目标是:将goreportcard
和golangci-lint
编译好,尽可能较小镜像的体积。因此第一次尝试,我使用了分阶段编译,用golang:1.14.1
编译,alpine
来发布。
基本 Dockerfile 如下:
# building stage
FROM golang:1.14-alpine3.11 as build
WORKDIR /tmp/build
COPY . .
RUN export GOPROXY="https://goproxy.cn,direct" \
&& go mod download \
&& go build -o app ./cmd/goreportcard-cli/ \
&& go get github.com/golangci/golangci-lint && go install github.com/golangci/golangci-lint/cmd/golangci-lint
# release stage
FROM golang:1.14-alpine3.11 as release
WORKDIR /app/goreportcard
COPY --from=build /tmp/build/app .
COPY --from=build /tmp/build/tpl ./tpl
COPY --from=build /tmp/build/assets ./assets
# FIXED: 不能使用golangci-lint, `File not found` 错误
COPY --from=build /go/bin/golangci-lint /usr/local/bin
EXPOSE 8000
ENTRYPOINT ["./app", "start-web", "&"]
问题清单和解决方案
#
由于并不是所有的问题都和Docker有关,因此我会使用 [分类]
在标题上注明。
...
March 29, 2020
redis主从复制是高可用方案中的一部分,那主从复制是如何进行的?又是如何实现的?怎么支撑了redis的高可用性?在主从模式下Master和Slave节点分别做了哪些事情?
redis高可用方案是什么?
#
我理解的redis高可用的特点有:
- 高QPS,主从 => 读写分离
- 高容量,集群分片 => 高容量
- 故障转移,sentinel => 故障转移
- 故障恢复,数据持久 => 故障恢复 ~ 这里我简单的理解(RDB + AOF)= 故障恢复
主从复制
#
redis 主从复制有两个版本:旧版(Ver2.8-),新版(Ver2.8+,增加PSYNC命令来解决旧版中的问题)
讨论复制时都需要考虑两种场景:
- 场景1:从节点刚刚上线,需要去同步主节点时,这部分可以理解为 全量复制。
- 场景2:从节点掉线,恢复上线后需要同步数据,使自己和主节点达到一致状态。这部分在旧版复制里等价于全量复制,在新版里可以理解为增量复制。
当然你肯定会想到如果主节点掉线,这时候会怎么样?这个场景当然也在redis高可用方案中,之时不是本文的重点,属于Sentinel机制的内容了。
旧版主从复制
#
前文说过了,旧版主从复制只有全量复制用于应付上述两个场景,因此下面的流程也只有一份:
- 从服务器向主服务器发送sync命令。
- 主服务器在收到sync命令之后,调用bgsave命令生成最新的rdb文件,将这个文件同步给从服务器,这样从服务器载入这个rdb文件之后,状态就会和主服务器执行bgsave命令时候的一致。
- 主服务器将保存在命令缓冲区中的写命令同步给从服务器,从服务器执行这些命令,这样从服务器的状态就跟主服务器当前状态一致了。
如果你不知道redis中还有个缓冲区的话,建议系统的了解下redis中缓冲区的设计。这里缓冲区特指命令缓冲区,后面还会讲到复制缓冲区。
但是这样的实现在 场景2 下的缺点很明显:如果说从节点断线后迅速上线,这段时间内的产生的写命令很少,却要全量复制主库的数据,传输了大量重复数据。
SYNC命令产生的消耗:
1. 主节点生成RDB,需要消耗大量的CPU,内存和磁盘IO
2. 网络传输大量字节数据,需要消耗主从服务器的网络资源
3. 从节点需要从RDB文件恢复,会造成阻塞无法接受客户端请求
优点就是:简单暴力。个人看来在redis架构中不合适的用法,不代表说实际场景中也一定不合适,简单暴力也是一个很大的优点。
新版主从复制
#
新版的主从复制跟旧版的区别就在于:对场景2的优化。
场景2的缺点上文已经提到过了,那么优化的方向就是**“尽量不使用全量复制;增加增量复制(PSYNC)的功能”**。为此还要解决下列问题:
- 如果某个从节点断线了,重新上线该从节点如何知道自己是否应该全量还是增量复制呢?
- 该从节点断线恢复后,又怎么知道自己缺失了哪些数据呢?
- 主节点又如何补偿该从节点在断线期间丢失的那部分数据呢?旧版的复制除了RDB,还有从命令缓冲区中的写命令来保持数据一致。
为此新版中使用了以下概念:
运行ID - runid
#
每个redis服务器都有其runid,runid由服务器在启动时自动生成,主服务器会将自己的runid发送给从服务器,而从服务器会将主服务器的runid保存起来。从服务器redis断线重连之后进行同步时,就是根据runid来判断同步的进度:
- 如果前后两次主服务器runid一致,则认为这一次断线重连还是之前复制的主服务器,主服务器可以继续尝试部分同步操作。
- 如果前后两次主服务器runid不相同,则全同步流程。
复制偏移量 - offset
#
主从节点,分别会维护一个复制偏移量:
主服务器每次向从服务器同步了N字节数据之后,将修改自己的复制偏移量+N。从服务器每次从主服务器同步了N字节数据之后,将修改自己的复制偏移量+N。通过对比主从节点的偏移量很容易就可以发现,主从节点是否处于一致状态。
...
January 8, 2020
背景
#
生产环境数据库不允许直接访问,但是又经常有需要直接操作数据库的需求😂。先不说合不合理,背景就是这个背景,因此只能通过跳板机来连接数据库,一(就)般(我)来(而)说(言)会使用ssh隧道,就轻松能解决这个问题,然鹅,事情并不简单。这里陈述一下:
- 生产环境数据库不让直接访问;
- 跳板机上没有公钥,没有权限;
- 我一次可能需要开3+个隧道才能启动服务【敲重点】
解决
#
本着“我不造轮子,谁来造轮子”的想法,这里就造一个小轮子:用Go
来实现SSH隧道多开,并支持配置。成果预览:

原理简要分析
#
如果代理原理有点了解,这里的原理差不多是一样的:Local <-> SSH tunnel <-> Remote Server,对于隧道来说把Local的请求传给Remote, 把Remote的响应告诉Local。直接上代码:
// Start .
// TODO: support random port by using localhost:0
func (tunnel *SSHTunnel) Start() error {
listener, err := net.Listen("tcp", tunnel.LocalAddr)
if err != nil {
return err
}
defer listener.Close()
// tunnel.Local.Port = listener.Addr().(*net.TCPAddr).Port
for {
conn, err := listener.Accept()
if err != nil {
return err
}
logger.Infof(tunnel.name() + " accepted connection")
go tunnel.forward(conn)
}
}
// 创建隧道并传递消息,分别有两个端点,一个是本地隧道口,另一个是远程服务器上的隧道口
func (tunnel *SSHTunnel) forward(localConn net.Conn) {
// 创建本地到跳板机的SSH连接
serverSSHClient, err := ssh.Dial("tcp", tunnel.ServerAddr, tunnel.SSHConfig)
if err != nil {
logger.Infof(tunnel.name()+" server dial error: %s", err)
return
}
logger.Infof(tunnel.name()+" connected to server=%s (1 of 2)", tunnel.ServerAddr)
// 创建跳板机到远程服务器的连接
remoteConn, err := serverSSHClient.Dial("tcp", tunnel.RemoteAddr)
if err != nil {
logger.Infof(tunnel.name()+" remote dial error: %s", err)
return
}
logger.Infof(tunnel.name()+" connected to remote=%s (2 of 2)", tunnel.RemoteAddr)
copyConn := func(writer, reader net.Conn) {
_, err := io.Copy(writer, reader)
if err != nil {
logger.Infof(tunnel.name()+" io.Copy error: %s", err)
}
}
// local(w) => 远程(r)
go copyConn(localConn, remoteConn)
// 远程(w) => 本地(r)
go copyConn(remoteConn, localConn)
}
代码中需要注意的是:
...
September 21, 2019
一些名词
#
MTU(Maximum Transmission Unit)
#
the maximum transmission unit (MTU) is the size of the largest protocol data unit (PDU) that can be communicated in a single network layer transaction. ——from wiki
MTU 物理接口(数据链路层)提供给其上层(通常是IP层)最大一次传输数据的大小。一般来说MTU=1500byte
。如果MSS + TCP首部 + IP首部 > MTU
,那么IP报文就会存在分片,否则就不需要分片。
MSS (Maximum Segment Size)
#
The maximum segment size (MSS) is a parameter of the options field of the TCP header that specifies the largest amount of data, specified in bytes, that a computer or communications device can receive in a single TCP segment. ——from wiki
...
April 1, 2019
Golang编写的热重载工具,自定义命令,支持监视文件及路径配置,环境变量配置。这是一个重复的轮子~
安装使用
#
go install github.com/yeqown/go-watcher/cmd/go-watcher
命令行
#
➜ go-watcher git:(master) ✗ ./go-watcher -h
NAME:
go-watcher - A new cli application
USAGE:
go-watcher [global options] command [command options] [arguments...]
VERSION:
2.0.0
AUTHOR:
[email protected]
COMMANDS:
init generate a config file to specified postion
run execute a command, and watch the files, if any change to these files, the command will reload
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help
--version, -v print the version
配置文件
#
watcher: # 监视器配置
duration: 2000 # 文件修改时间间隔,只有高于这个间隔才回触发重载
included_filetypes: # 监视的文件扩展类型
- .go #
excluded_regexps: # 不被监视更改的文件正则表达式
- ^.gitignore$
- '*.yml$'
- '*.txt$'
additional_paths: [] # 除了当前文件夹需要额外监视的文件夹
excluded_paths: # 不需要监视的文件名,若为相对路径,只能对于当前路径生效
- vendor
- .git
envs: # 额外的环境变量
- GOROOT=/path/to/your/goroot
- GOPATH=/path/to/your/gopath
使用范例日志
#
➜ go-watcher git:(master) ✗ ./package/osx/go-watcher run -e "make" -c ./config.yml
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/cmd) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/cmd/go-watcher) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/command) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/log) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata/exclude) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata/testdata_inner) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/resources) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils/testdata) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils/testdata/testdata_inner) is under watching
rm -fr package
go build -o package/osx/go-watcher cmd/go-watcher/main.go
GOOS=linux GOARCH=amd64 go build -o package/linux/go-watcher cmd/go-watcher/main.go
mkdir -p package/archived
tar -zcvf package/archived/go-watcher.osx.tar.gz package/osx
a package/osx
a package/osx/go-watcher
tar -zcvf package/archived/go-watcher.linux.tar.gz package/linux
a package/linux
a package/linux/go-watcher
[INFO] command executed done!
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx/go-watcher) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux/go-watcher) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived/go-watcher.linux.tar.gz) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/VERSION) is skipped, not target filetype
[INFO] [/Users/yeqown/Projects/opensource/go-watcher/cmd/go-watcher/main.go] changed
rm -fr package
mkdir -p package/osx
mkdir -p package/linux
echo "2.0.0" > VERSION
cp VERSION package/osx
cp VERSION package/linux
go build -o package/osx/go-watcher cmd/go-watcher/main.go
GOOS=linux GOARCH=amd64 go build -o package/linux/go-watcher cmd/go-watcher/main.go
mkdir -p package/archived
tar -zcvf package/archived/go-watcher.osx.tar.gz package/osx
a package/osx
a package/osx/go-watcher
a package/osx/VERSION
tar -zcvf package/archived/go-watcher.linux.tar.gz package/linux
a package/linux
a package/linux/go-watcher[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is skipped, not target filetype
a package/linux/VERSION
[INFO] command executed done!
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/VERSION) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
^C[INFO] quit signal captured!
[INFO] go-watcher exited
➜ go-watcher git:(master) ✗
June 28, 2018
“熟悉http协议”,肯定很多IT小伙伴都在招聘岗位上看得到过,但是怎么才叫熟悉http协议呢?抽空梳理了一下,也算是对这一部分知识的笔记吧!
可能对于大部分人来说,网络web编程就是使用一些第三方库来进行请求和响应的处理,再多说一点就是这个URI
要使用POST
方法,对于携带的数据需要处理成为formdata
。
基础知识
#
Q1: HTTP协议是什么?用来干什么?
HTTP协议是基于TCP/IP协议的应用层协议,主要规定了客户端和服务端之间的通信格式。主要作用也就是传输数据(HTML,图片,文件,查询结果)。
#网络分层
#
互联网的实现分成了几层,如何分层有不同的模型(七层,五层,四层),这里按五层模型来解释:
(靠近用户)应用层 < 传输层 < 网络层 < 链接层 < 物理层(靠近硬件)
层级 |
作用 |
拥有协议 |
物理层 |
传送电信号0 1 |
无 |
数据链路层 |
定义数据包;网卡MAC地址;广播的发送方式; |
Ethernet 802.3; Token Ring 802.5 |
网络层 |
引进了IP地址,用于区分不同的计算机是否属于同一网络 |
IP; ARP; RARP |
传输层 |
建立端口到端口的通信,实现程序时间的交流,也就是socket |
TCP; UDP |
应用层 |
约定应用程序的数据格式 |
HTTP; FTP; DNS |
每一层级,都是解决问题而诞生的,也就是他们各自作用对应的问题,推荐参考资料中的“互联网协议入门”。
#HTTP通信流程
#

#拓展–三次握手和四次挥手
#
经常在其他地方看到这些,一直不知道了解这部分有什么用,但是syn Flood
攻击,恰恰是利用了TCP三次握手中的环节。利用假IP伪造SYN
请求,服务端会多次尝试发送SYN-ACK
给客户端,但是IP并不存在也就无法成功建立连接。在一定时间内伪造大量这种请求,会导致服务器资源耗尽无法为正常的连接服务。(注:服务器SYN连接数量有限制,SYN-ACK超时重传机制)
三次握手流程:
- The client requests a connection by sending a SYN (synchronize) message to the server.
- The server acknowledges this request by sending SYN-ACK back to the client.
- The client responds with an ACK, and the connection is established.

...
June 8, 2018
背景和目标
#
背景
#
项目需要在现有项目的基础上实现权限系统,但为了低耦合,选择实现了一个基于ne7ermore/gRBAC的auth-server,用于实现权限,角色,用户的管理,以及提供鉴权服务。在开发环境对接没有问题,正常的鉴权访问。到了线上部署的时候,才发现:
- 线上某服务部署在多台机器上;
- 目前的api-gateway并不支持同一服务配置多个node;
想的办法有:
序号 |
描述 |
优点 |
缺点 |
1 |
api-gateway通过url来转发请求,之前是配置IP加端口 |
api-gateway改动小 |
影响web和APP升级 |
2 |
api-gateway能支持多台机器,并进行调度 |
api-gateway功能更强大,把以后要做的事情提前做好基础 |
好像没啥缺点,只是费点时间支持下多节点配置,并调度 |
如果没说清,请看下图:

目标
#
那么,目标也就明确了,需要实现api-gateway中实现基于权重的调度。为啥要基于权重?其一是仿照nginx基于权重的负载均衡,其二是服务器性能差异。
轮询调度算法介绍
#
轮询调度算法:
#
轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。该算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
假设有一组服务器N台,S = {S1, S2, …, Sn},一个指示变量i表示上一次选择的服务器ID。变量i被初始化为N-1。其算法如下:
j = i;
do {
j = (j + 1) mod n;
i = j;
return Si;
} while (j != i);
return NULL;
平滑加权轮询调度算法:
#
上述的轮询调度算法,并没有考虑服务器性能的差异,实际生产环境中,每一台服务器配置和安装的业务并不一定相同,处理能力不完全一样。因此需要根据服务器能力,分配不同的权值,以免服务的超负荷和过分闲余。
...
May 18, 2018
需要的技术及工具:
- Python3 + Selenuium
- Golang net/http
- React-Native 相关(使用了react-navigation)
- MongoDB
- Redis
代码地址:
项目构思及构成
#
食谱类型的App,应用市场肯定有更好的的食谱APP。所以自己开发的目的,首先是写代码,其次是定制APP~
好的,现在化身产品经理,设计一下APP有哪些功能:
- 每日菜谱推荐,推荐可更换
- 每天需要准备的材料提醒
- 发现更多菜谱
- 分类筛选菜谱
- 搜索菜谱
- 查看菜谱详情
- 设置(不知道设置啥,提前准备吧)
设计稿?不存在的,随心所欲。
现在分析下我需要做的事情:
- 能跑起来的APP,与restful web api 交互。
- 能跑起来的web-api,提供菜谱数据,筛选,推荐,搜索等功能
- 能跑起来的简易spider,从网上获取菜谱信息。(这个爬虫能解析动态生成网站就够用了,姑且称之为爬虫吧)
没有考虑大量数据,因此爬虫并不通用,只适合特定XX网站。
实战爬虫
#
这个APP里面最重要的就是菜谱数据了,那么开发之前,需要明确的数据格式,如下:
{
"name": "name",
"cat": "cat",
"img": "img_url",
"mark_cnt": 19101,
"view_cnt": 181891,
"setps": [
{
"desc": "",
"img": "",
},
// more step
],
"material": {
"ingredients": [
{
"name": "ingredients_name",
"weight": "ingredients_weight",
},
// more ingredients
],
"seasoning": [
{
"name": "seasoning_name",
"weight": "seasoning_weight",
},
// more seasoning
],
},
"create_time": "2018xxxxxx",
"update_time": "2018xxxxxx",
}
目标
#
前提:无法直接获取到该网站的服务API,才使用爬虫间接获取数据。
...