背景:需要向一个文件中写入 1 亿个 ip ,最快的方法是什么?机器配置是 mac book,双核 cpu,8GB 内存。
我用 java 实现了一个多线程的写入,发现速度慢的要死(写入时间 5 分钟以上)。有没有人推荐一下速度写入大文件的方法,或者其他语言快速写入大文件的方案。

不是顺序 ip ,是随机 ip 。

看了老哥们的发言,受到很多启发。Ip 确实是一个 32 位无符号整形,因此有很多老哥推荐写入一个 32 位无符号整数代替 ip 字符串。我想问的是,你们的真实场景中有将 ip 保存为 32 位整数的吗?你工作中存储 ip 的地方有用整数代替 ip 的吗?日志中,或者是存储中。

mmap

感觉减少磁盘 io ,内存多放点

写文件,不需要多线程,瓶颈必然在 IO ,简单的增加缓冲区即可。

最快的话……VFS
其次内存映射
写入不要多线程,只会降低速度

楼上都说了,写内存,减少 IO

kafka 为啥这么快?利用了顺序写入!

IO 操作多线程一般没什么效果,硬盘都是连续大量写入时要更快。
你需要的是缓存方案,简单点就是先往内存中存着,再从内存中整块整块写入硬盘

拼成一个大文本一次写进去,拿 PHP 试了下 5 秒不到,别搞花里胡哨的了,内存要不够就拆成几次

不知道 OP 能不能意识到这问题背后有多少坑……

  1. 多线程读写同一个 io 很蠢且不会起作用。因为文件系统 api 的作用仅仅是给内核发个通知,让内核去 copy 数据。多线程和单线程调用同一个 file descriptor 上的读写不会有什么区别
  2. 1 亿个 ip 不应该放在一个普通文件里,这么大规模的数据为什么不用数据库管理?
  3. ipv4 原本仅仅只是一个 4 字节的整数,ipv6 也只有 16 字节,不知道 OP 准备存的是什么,不会是字符串吧
  4. 这种规律排布的数据压缩比本来可以非常大的,考虑压缩了吗

最快的方法是开一个 file map (mmap) ,然后在 mmap 给你提供的「内存」里存数据。你往里写的时候写的是内存,然后 OS 内核会自动帮你把内存页交换到文件系统上。

但这个映射也还是可以继续优化的,因为默认内存页很小且物理内存不连续。如果你想,用上巨型页和手撸的 dma 驱动可以更快

不知道,没写过

卧槽,1 亿个 IP 拼的字符串,你内存没爆吗?

一个 ip 占一个 int ,也就 4 byte ,搞什么花里胡哨的。

用 python 写了个最暴力的, 也就 20s...,还用的 wsl 跑的,如果用原生 windows 估计更快。

代码

with open("./ip.txt", "w") as fp:
 for i in range(0, 10000):
 for j in range(0, 10000):
 fp.write(str(i * j) + "\n")

结果

➜ Desktop time python test.py
python test.py 19.75s user 0.81s system 99% cpu 20.586 total

看了下楼上的回复,用 mmap 实现了一把,单线程 50s 写完。还能继续提升这个速度吗?比方说多线程 mmap?

你生成一个合法的 ip 啊,你写个 str(i*j)这。。。

不懂为什么会爆内存,一亿个 IP 的原始数据也就四亿字节,380MB 左右,就算用字符串,全部按 3*4+3+1 (四段+三个点+换行符)计算 16 亿字节,也就 1.5GB 左右

i * j 可以理解为一个 int 啊,就是一个 ip

好奇老哥为啥说提升性能就上多线程?多线程不是万能解药

好思路。我以前只是写 ip 字符串。如果把所有的 ip 拼成一个字符串一次写入,用多久。

别惦记你那多线程啦,首先,为什么要写这么多 ip 到文件,其次,如果是连续 ip 可以考虑压缩

因为我单线程写花了 100s,多线程写花了 50s 。你能说性能没有提升吗?

因为我刚才写字符串,就把内存干爆了。

IPV4 其实就是个 32 位无符号整型,1 亿也就只需要 100M ,写文件几秒钟就好了呀。

最佳情况,1.5GB 总文件大小,假设是机械硬盘每秒 100MB 的顺序写入速度,最快大概 15 秒能解决问题。

最差情况,如果你是需要随机写,按机械硬盘每秒 100 次 iops 来算,全部乱序最久需要 11 天。

现在的问题是,你需要把需求中的随机,最大程度改为顺序。

因此,先在内存中,随机把数据都整理后,最后一次或几次顺序写入文件,这才是上策。如果内存一次性放不下 1.5G ,可以把这 1.5G 拆成几块,每次整理,最后用外排序,速度也慢不到哪里去。

哈哈哈,PHP 有官方的 ip2long 和 long2ip
根据这个思路,先转成 long ,再拼接储存应该会快很多

IO 操作多线程性能没提升已经是历史了, 现在部分介质并发写性能会更高的

一秒不到

写到内存时间 ms:313
从内存写到硬盘时间 ms:572
总时间 ms:885

代码有点毛病,应该是一次写 4 字节一个 IP ,但是结果不会有太大区别。

用 bitmap ,ip 转 int 当索引,存在设置为 1 ,不存在就是 0 ,然后序列化就行。

创建一个 int 数组,然后 shuffle ,目的是去重,毕竟 ipv4 总共才 42 亿,直接随机容易重复。然后把 int 转成 ipv4 字符串再写出,在我的服务器上大概 6 秒。

init array 117.512617ms
shuffle array 3.546346202s
write to buffer 5.151557182s
write to file 5.613269245s

有没有可能是你写的代码有问题,而不是多线程能比单线程有提升。或者多线程没提升在写入的过程中而是提升在了奇奇怪怪的代码逻辑上。

println 然后重定向到文件?

写 1 亿没意思,连磁盘缓存都没打满……写个 10 亿吧
即使是随机数据,使用 zlib 这样的库也能极大的压缩数据

1e8, zlib vs raw, 810ms vs 310 ms
1e9, zlib vs raw, 11s vs 3.73s

gist.github.com/yuguorui/9700aa236dcb6e7072de282310d3b866  显示 Gist 代码 

额,后面的时间写反了,zlib 是快的那个哈。

你们的真实场景中有将 ip 保存为 32 位整数的吗?

IP 本来就是 32 位整数…所以不转换的原始格式就是整数…只不过很多情况我们会把它转换成字符串表示,那是为了方便人去阅读,如 1.1.1.1 这样…一些日志也是为了方便人阅读,做了一个转换。楼主还是没转换过来思维

我想问的是,你们的真实场景中有将 ip 保存为 32 位整数的吗?
有,因为很多场景要做 ip 段判断

你工作中存储 ip 的地方有用整数代替 ip 的吗?
如上所述数据库会存为 int
日志打印的时候还是方便人看的字符串

先把 ip 转成 unsigned int 不就完事了?
def ipToInt(ip):
ipList = ip.split(".")
seg0 = int(ipList[0]) << 24
seg1 = int(ipList[1]) << 16
seg2 = int(ipList[2]) << 8
seg3 = int(ipList[3])
return seg0 | seg1 | seg2 | seg3

最近在学习世界最好的语言 PHP ,用 PHP 试了一下

1.把 ip 转换成 int 遍历到数组中 花费 2800 ms
2.把数组 implode ,写入到文件中 花费 1800 ms

随机 ip ,你这是顺序 ip 吧

好快的速度,老哥,能看一下代码吗?

这个数据量就直接往内存怼就可以了,高深的我不会...

package main

import (
"fmt"
"io/ioutil"
"math/rand"
"time"
)

func ubtoa(dst []byte, start int, v byte) int {
if v < 10 {
dst[start] = v + '0'
return 1
} else if v < 100 {
dst[start+1] = v%10 + '0'
dst[start] = v/10 + '0'
return 2
}

dst[start+2] = v%10 + '0'
dst[start+1] = (v/10)%10 + '0'
dst[start] = v/100 + '0'
return 3
}

func main() {
rand.Seed(time.Now().Unix())

t := time.Now()

arr := make([]int, 100000000)
for i := range arr {
arr[i] = i
}
fmt.Println("init array", time.Since(t))

rand.Shuffle(len(arr), func(i, j int) { arr[i], arr[j] = arr[j], arr[i] })
fmt.Println("shuffle array", time.Since(t))

b := make([]byte, 16*100000000)
pos := 0
for i := range arr {
pos += ubtoa(b, pos, byte(arr[i]>>24))
b[pos] = '.'
pos++

pos += ubtoa(b, pos, byte(arr[i]>>16))
b[pos] = '.'
pos++

pos += ubtoa(b, pos, byte(arr[i]>>8))
b[pos] = '.'
pos++

pos += ubtoa(b, pos, byte(arr[i]))
b[pos] = '\n'
pos++
}
fmt.Println("write to buffer", time.Since(t))

ioutil.WriteFile("ip.txt", b[:pos], 0600)
fmt.Println("write to file", time.Since(t))
}

回复好像不支持格式

1 亿个 IP ,就是 4 亿个字节,直接申请 4 亿 byte 内存,往里面写入随机 uint32 数字,然后一次性写入文件,我本机测试结果大概 2000ms 左右。
Golang 代码:

package main

import (
"encoding/binary"
"math/rand"
"os"
"time"
)

func main() {

start := time.Now().UnixMilli()

size := int(4e8)
buf := make([]byte, size)
for i := 0; i < size; i += 4 {
binary.BigEndian.PutUint32(buf[i:], rand.Uint32())
}

err := os.WriteFile("iptest.bin", buf, os.ModePerm)
if err != nil {
panic(err)
}
println("elapsed:", time.Now().UnixMilli()-start)
}

当然是可以保存啊,问题你保存 Ip 肯定是为了看的是吧,或者为了查询。我有个疑问,究竟是保存数字更容易查询呢还是报错字符串更容易查询呢?因为我接触的项目比较少,一般都是保存为字符串。所以比如我想查询 192 字段的 ip ,可能前缀匹配就行了。如果保存数字的话,也支持这种查询吗?

感谢!

#43 我也没有项目经验,就说点自己的感受。ip 字符串是给人看的,写少量日志文件我倾向于用字符串,但需要放数据库我会用数字。至于这个数量级的查询还要匹配 ip range ,那我觉得还是适合用数据库,mysql 支持 INET_ATON ,匹配 192.0.0.0 到 192.255.255.255 对应的数字范围即可。

此贴可以结了。Lz 原来的想法只是向文件写入 ip 字符串。最终采用的是写入 byte 数组,每 4 位表示一个 ip 。最终也没用使用 MMAP 。获益匪浅!

我觉得数字无论从 IP 段匹配还是 IP 段搜索都更容易,32bit 和 IPv4 是完全的一一对应关系,但 String 和 IP 并不是一一对应,因为并不是所有 String 都是 IP ; IPv6 就更复杂了,还会有多个 String 对应一个 IPv6 的问题(如果硬要杠的话 IPv4 也有多对一问题,比如 1.1 、1.0.0.1 、001.0.0.001 都是一个 IP )。你要用 String 存,数据库读写是不是得做校验?

数字因为是二进制,IPv4 可以划分为四组每组 8 个 bit 。

一般只有日志、以及代码中的临时变量存 String ,要入数据库还是存数字稳妥

1.1

使用 INET_ATON 运算会不会在高流量下对数据库造成压力?我记得在去哪的 mysql 数据库设计规范上明确指出,要尽可能的少使用函数运算。我找了下文档,结果出现了令我疑惑的一点,设计规范如下:
1.禁止在 MySQL 中进行数学运算和函数运算。

2.建议使用 INT UNSIGNED 存储 IPV4 。
UNSINGED INT 存储 IP 地址占用 4 字节,CHAR(15)则占用 15 字节。另外,计算机处理整数类型比字符串类型快。使用 INT UNSIGNED 而不是 CHAR(15)来存储 IPV4 地址,通过 MySQL 函数 inet_ntoa 和 inet_aton 来进行转化。IPv6 地址目前没有转化函数,需要使用 DECIMAL 或两个 BIGINT 来存储。

难道 inet_aton 不属于函数运算吗?

关键是先尽量放在内存,批量 flush 到硬盘,这时候就要看硬盘 IO 如何了

#49 上面所说是在需要手动查询日志时保持查询语句的可读性,“高流量”那我肯定是把 inet_aton 放在程序里而不是放在 sql 查询语句里

有道理!我刚才也想到了这一点!

这个任务肯定是被硬盘的顺序写性能 bound 住啊……就是你把要写的东西一段段 buffer 准备好然后直接 flush 就能达到最大值啊……如果是 SSD 可能还有 io depth 可以优化但是速度应该也差别不大……

多谢分享,求问这些知识哪里能系统学习到?比方说文件系统 api ,ip 的数据结构,以及 mmap 和之后说的各种优化?

好多都是学校里的了,因为学校里你有时间,可以很纯粹地为了学习而学习。你可以 TCP/IP 详解一二三卷硬看,虽然最后可能没记住多少,但看过了哪些目录一定是有印象的,这跟从目的出发一点一点零碎地搜就完全不一样了。

在学校你也有机会看一整本自制 OS 教程,一整本 Linux 教程,一整本 windows 核心编程,你如果说「系统地」怎么学的,那就是书籍目录,没有比这个更系统的了。

有了这些印象作为框架,即便是搜索新东西也会更有头绪一些。比方说 OP 这个例子,假设他其实想解决的问题其实是「要对大量 IP 进行匹配」,你就能想到协议栈,想到 win 和 nix 的内核,就会联想能不能手写逻辑简化数据结构的处理,以及联想到能不能别用内核的协议栈——复制数据和切换内核态都很耗时间。然后你就能用 high performance 「 USER SPACE 」 tcp 「 STACK 」搜到 DPDK (虽然我是从前公司项目知道它的),而搜 high performance tcp 就很难找到这种方案

我想大多数人都不缺书单,缺的是时间和精力真的去看一遍…… 工作之后自己都有点开始对「为了学习而学习」不屑一顾了

每个 IP 最大长度 15 Bytes ,一亿个 IP 的容量为: 100000000 * 15Bytes ,即 1.39G ,现在的硬盘写入速度几个 G/S 的很多,机械硬盘也是 100+M/S ,可以一个线程分段生成,交给另一个异步线程写入 (不要多个线程同时写入)。

多线程写磁盘。。。

多谢,果然基础知识重要

前面有说不要多线程写入的???除非你用的是机械硬盘才这样,ssd 一直都是并发写效率最高。

“你们的真实场景中有将 ip 保存为 32 位整数的吗?”
这个一般下意识都会选择存 4bytes 的,存字符串的方式按道理说除非有明文要求,都不会去考虑的

很好,面试官由悄悄 get 一个八股

没错,打好基础后,对新技术稍作了解即可。要用什么新技术了时临时抱佛脚学一下会用就行,用不到的东西没必要花时间去学。所以别人问我要学什么,我都告诉他没有用不要学。。。

深耕某个领域,自然会步步深入专研,这都不仅仅是学习的范畴了。

没人考虑 ipv6 么

瞎写了写,不知道对不对

public class Main {

public static void main(String[] args) {
byte[][] ip = new byte1000000;
long start = System.currentTimeMillis();
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("D:\Cache\ip.bin"));
int count = 0;
for (int i = 0; i < 255; i++) {
for (int j = 0; j < 255; j++) {
for (int k = 0; k < 255; k++) {
for (int l = 0; l < 255; l++) {
ipcount = (byte) i;
ipcount = (byte) j;
ipcount = (byte) k;
ipcount = (byte) l;
count++;
if (count % 1000000 == 0) {
for (int m = 0; m < 1000000; m++) {
bufferedOutputStream.write(ip[m]);
}
count = 0;
}
}
}
}
}
bufferedOutputStream.close();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}

时间:144663

占用空间:15.7 GB (16,912,003,072 字节)