V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lsk569937453
V2EX  ›  程序员

如何快速向文件中写入 1 亿个 ip?

  •  
  •   lsk569937453 · 2022-04-09 15:49:53 +08:00 · 7758 次点击
    这是一个创建于 959 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    第 1 条附言  ·  2022-04-09 17:16:45 +08:00
    不是顺序 ip ,是随机 ip 。
    第 2 条附言  ·  2022-04-09 17:32:09 +08:00
    看了老哥们的发言,受到很多启发。Ip 确实是一个 32 位无符号整形,因此有很多老哥推荐写入一个 32 位无符号整数代替 ip 字符串。我想问的是,你们的真实场景中有将 ip 保存为 32 位整数的吗?你工作中存储 ip 的地方有用整数代替 ip 的吗?日志中,或者是存储中。
    66 条回复    2022-08-14 18:54:35 +08:00
    Buges
        1
    Buges  
       2022-04-09 15:52:16 +08:00 via Android   ❤️ 1
    mmap
    Itesting
        2
    Itesting  
       2022-04-09 15:52:38 +08:00 via iPhone
    感觉减少磁盘 io ,内存多放点
    gam2046
        3
    gam2046  
       2022-04-09 15:53:02 +08:00
    写文件,不需要多线程,瓶颈必然在 IO ,简单的增加缓冲区即可。
    ChaosesIb
        4
    ChaosesIb  
       2022-04-09 15:54:56 +08:00
    最快的话……VFS
    其次内存映射
    写入不要多线程,只会降低速度
    winnie2012
        5
    winnie2012  
       2022-04-09 15:57:08 +08:00
    楼上都说了,写内存,减少 IO
    pengtdyd
        6
    pengtdyd  
       2022-04-09 15:57:09 +08:00
    kafka 为啥这么快?利用了顺序写入!
    xiri
        7
    xiri  
       2022-04-09 15:57:19 +08:00   ❤️ 1
    IO 操作多线程一般没什么效果,硬盘都是连续大量写入时要更快。
    你需要的是缓存方案,简单点就是先往内存中存着,再从内存中整块整块写入硬盘
    Puteulanus
        8
    Puteulanus  
       2022-04-09 16:08:25 +08:00
    拼成一个大文本一次写进去,拿 PHP 试了下 5 秒不到,别搞花里胡哨的了,内存要不够就拆成几次
    GeruzoniAnsasu
        9
    GeruzoniAnsasu  
       2022-04-09 16:17:13 +08:00   ❤️ 19
    不知道 OP 能不能意识到这问题背后有多少坑……

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



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

    但这个映射也还是可以继续优化的,因为默认内存页很小且物理内存不连续。如果你想,用上巨型页和手撸的 dma 驱动可以更快
    HankLu
        10
    HankLu  
       2022-04-09 16:24:01 +08:00
    不知道,没写过
    lsk569937453
        11
    lsk569937453  
    OP
       2022-04-09 16:25:18 +08:00
    @Puteulanus 卧槽,1 亿个 IP 拼的字符串,你内存没爆吗?
    zmxnv123
        12
    zmxnv123  
       2022-04-09 16:37:43 +08:00
    一个 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
    ```
    lsk569937453
        13
    lsk569937453  
    OP
       2022-04-09 16:38:04 +08:00
    看了下楼上的回复,用 mmap 实现了一把,单线程 50s 写完。还能继续提升这个速度吗?比方说多线程 mmap?
    lsk569937453
        14
    lsk569937453  
    OP
       2022-04-09 16:39:34 +08:00
    @zmxnv123 你生成一个合法的 ip 啊,你写个 str(i*j)这。。。
    xiri
        15
    xiri  
       2022-04-09 16:42:00 +08:00 via Android   ❤️ 2
    @lsk569937453 不懂为什么会爆内存,一亿个 IP 的原始数据也就四亿字节,380MB 左右,就算用字符串,全部按 3*4+3+1 (四段+三个点+换行符)计算 16 亿字节,也就 1.5GB 左右
    zmxnv123
        16
    zmxnv123  
       2022-04-09 16:47:40 +08:00
    @lsk569937453 i * j 可以理解为一个 int 啊,就是一个 ip
    shawnsh
        17
    shawnsh  
       2022-04-09 16:54:32 +08:00 via Android
    @lsk569937453 好奇老哥为啥说提升性能就上多线程?多线程不是万能解药
    lsk569937453
        18
    lsk569937453  
    OP
       2022-04-09 17:05:45 +08:00
    @zmxnv123 好思路。我以前只是写 ip 字符串。如果把所有的 ip 拼成一个字符串一次写入,用多久。
    Juszoe
        19
    Juszoe  
       2022-04-09 17:10:14 +08:00
    别惦记你那多线程啦,首先,为什么要写这么多 ip 到文件,其次,如果是连续 ip 可以考虑压缩
    lsk569937453
        20
    lsk569937453  
    OP
       2022-04-09 17:10:53 +08:00
    @shawnsh 因为我单线程写花了 100s,多线程写花了 50s 。你能说性能没有提升吗?
    lsk569937453
        21
    lsk569937453  
    OP
       2022-04-09 17:13:15 +08:00
    @xiri 因为我刚才写字符串,就把内存干爆了。
    xuanbg
        22
    xuanbg  
       2022-04-09 17:21:37 +08:00
    IPV4 其实就是个 32 位无符号整型,1 亿也就只需要 100M ,写文件几秒钟就好了呀。
    documentzhangx66
        23
    documentzhangx66  
       2022-04-09 17:39:03 +08:00
    最佳情况,1.5GB 总文件大小,假设是机械硬盘每秒 100MB 的顺序写入速度,最快大概 15 秒能解决问题。

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

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

    因此,先在内存中,随机把数据都整理后,最后一次或几次顺序写入文件,这才是上策。如果内存一次性放不下 1.5G ,可以把这 1.5G 拆成几块,每次整理,最后用外排序,速度也慢不到哪里去。
    Features
        24
    Features  
       2022-04-09 17:41:35 +08:00
    哈哈哈,PHP 有官方的 ip2long 和 long2ip
    根据这个思路,先转成 long ,再拼接储存应该会快很多
    Inn0Vat10n
        25
    Inn0Vat10n  
       2022-04-09 17:42:27 +08:00
    IO 操作多线程性能没提升已经是历史了, 现在部分介质并发写性能会更高的
    oldshensheep
        26
    oldshensheep  
       2022-04-09 17:52:40 +08:00
    一秒不到
    <script src="https://gist.github.com/oldshensheep/9af3218c5b365d9cc33cba59b01265e3.js"></script>

    写到内存时间 ms:313
    从内存写到硬盘时间 ms:572
    总时间 ms:885
    oldshensheep
        27
    oldshensheep  
       2022-04-09 18:03:36 +08:00
    代码有点毛病,应该是一次写 4 字节一个 IP ,但是结果不会有太大区别。
    LeegoYih
        28
    LeegoYih  
       2022-04-09 19:10:01 +08:00   ❤️ 3
    用 bitmap ,ip 转 int 当索引,存在设置为 1 ,不存在就是 0 ,然后序列化就行。
    0o0O0o0O0o
        29
    0o0O0o0O0o  
       2022-04-09 19:19:53 +08:00 via iPhone
    创建一个 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
    clf
        30
    clf  
       2022-04-09 19:22:58 +08:00
    有没有可能是你写的代码有问题,而不是多线程能比单线程有提升。或者多线程没提升在写入的过程中而是提升在了奇奇怪怪的代码逻辑上。
    colatin
        31
    colatin  
       2022-04-09 19:49:57 +08:00
    println 然后重定向到文件?
    yuguorui96
        32
    yuguorui96  
       2022-04-09 20:35:10 +08:00
    写 1 亿没意思,连磁盘缓存都没打满……写个 10 亿吧
    即使是随机数据,使用 zlib 这样的库也能极大的压缩数据

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

    https://gist.github.com/yuguorui/9700aa236dcb6e7072de282310d3b866
    yuguorui96
        33
    yuguorui96  
       2022-04-09 20:37:47 +08:00
    @yuguorui96 额,后面的时间写反了,zlib 是快的那个哈。
    ZE3kr
        34
    ZE3kr  
       2022-04-09 21:12:35 +08:00 via iPhone   ❤️ 1
    你们的真实场景中有将 ip 保存为 32 位整数的吗?

    IP 本来就是 32 位整数…所以不转换的原始格式就是整数…只不过很多情况我们会把它转换成字符串表示,那是为了方便人去阅读,如 1.1.1.1 这样…一些日志也是为了方便人阅读,做了一个转换。楼主还是没转换过来思维
    iyaozhen
        35
    iyaozhen  
       2022-04-09 21:16:49 +08:00
    我想问的是,你们的真实场景中有将 ip 保存为 32 位整数的吗?
    有,因为很多场景要做 ip 段判断

    你工作中存储 ip 的地方有用整数代替 ip 的吗?
    如上所述数据库会存为 int
    日志打印的时候还是方便人看的字符串
    darknoll
        36
    darknoll  
       2022-04-09 21:21:54 +08:00
    @lsk569937453 先把 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
    Features
        37
    Features  
       2022-04-09 21:36:25 +08:00
    最近在学习世界最好的语言 PHP ,用 PHP 试了一下

    1.把 ip 转换成 int 遍历到数组中 花费 2800 ms
    2.把数组 implode ,写入到文件中 花费 1800 ms
    lsk569937453
        38
    lsk569937453  
    OP
       2022-04-09 22:03:42 +08:00
    @oldshensheep 随机 ip ,你这是顺序 ip 吧
    lsk569937453
        39
    lsk569937453  
    OP
       2022-04-09 22:08:36 +08:00
    @0o0O0o0O0o 好快的速度,老哥,能看一下代码吗?
    0o0O0o0O0o
        40
    0o0O0o0O0o  
       2022-04-09 22:14:46 +08:00
    @lsk569937453 这个数据量就直接往内存怼就可以了,高深的我不会...

    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))
    }
    0o0O0o0O0o
        41
    0o0O0o0O0o  
       2022-04-09 22:15:25 +08:00
    回复好像不支持格式
    none
        42
    none  
       2022-04-09 22:23:49 +08:00
    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)
    }
    lsk569937453
        43
    lsk569937453  
    OP
       2022-04-09 22:24:12 +08:00
    @ZE3kr 当然是可以保存啊,问题你保存 Ip 肯定是为了看的是吧,或者为了查询。我有个疑问,究竟是保存数字更容易查询呢还是报错字符串更容易查询呢?因为我接触的项目比较少,一般都是保存为字符串。所以比如我想查询 192 字段的 ip ,可能前缀匹配就行了。如果保存数字的话,也支持这种查询吗?
    lsk569937453
        44
    lsk569937453  
    OP
       2022-04-09 22:31:19 +08:00
    0o0O0o0O0o
        45
    0o0O0o0O0o  
       2022-04-09 22:36:13 +08:00 via iPhone
    @lsk569937453 #43 我也没有项目经验,就说点自己的感受。ip 字符串是给人看的,写少量日志文件我倾向于用字符串,但需要放数据库我会用数字。至于这个数量级的查询还要匹配 ip range ,那我觉得还是适合用数据库,mysql 支持 INET_ATON ,匹配 192.0.0.0 到 192.255.255.255 对应的数字范围即可。
    lsk569937453
        46
    lsk569937453  
    OP
       2022-04-09 22:37:11 +08:00
    此贴可以结了。Lz 原来的想法只是向文件写入 ip 字符串。最终采用的是写入 byte 数组,每 4 位表示一个 ip 。最终也没用使用 MMAP 。获益匪浅!
    ZE3kr
        47
    ZE3kr  
       2022-04-09 22:38:22 +08:00   ❤️ 1
    @lsk569937453 我觉得数字无论从 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 ,要入数据库还是存数字稳妥
    ZE3kr
        48
    ZE3kr  
       2022-04-09 22:39:01 +08:00
    https://1.1
    lsk569937453
        49
    lsk569937453  
    OP
       2022-04-09 22:48:00 +08:00
    @0o0O0o0O0o 使用 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 不属于函数运算吗?
    jetyang
        50
    jetyang  
       2022-04-09 22:52:20 +08:00
    关键是先尽量放在内存,批量 flush 到硬盘,这时候就要看硬盘 IO 如何了
    0o0O0o0O0o
        51
    0o0O0o0O0o  
       2022-04-09 22:56:19 +08:00 via iPhone
    @lsk569937453 #49 上面所说是在需要手动查询日志时保持查询语句的可读性,“高流量”那我肯定是把 inet_aton 放在程序里而不是放在 sql 查询语句里
    lsk569937453
        52
    lsk569937453  
    OP
       2022-04-09 22:58:23 +08:00
    @0o0O0o0O0o 有道理!我刚才也想到了这一点!
    zhoujinjing09
        53
    zhoujinjing09  
       2022-04-10 01:40:28 +08:00
    这个任务肯定是被硬盘的顺序写性能 bound 住啊……就是你把要写的东西一段段 buffer 准备好然后直接 flush 就能达到最大值啊……如果是 SSD 可能还有 io depth 可以优化但是速度应该也差别不大……
    levelworm
        54
    levelworm  
       2022-04-10 01:50:55 +08:00 via Android
    @GeruzoniAnsasu 多谢分享,求问这些知识哪里能系统学习到?比方说文件系统 api ,ip 的数据结构,以及 mmap 和之后说的各种优化?
    GeruzoniAnsasu
        55
    GeruzoniAnsasu  
       2022-04-10 02:28:22 +08:00   ❤️ 3
    @levelworm 好多都是学校里的了,因为学校里你有时间,可以很纯粹地为了学习而学习。你可以 TCP/IP 详解一二三卷硬看,虽然最后可能没记住多少,但看过了哪些目录一定是有印象的,这跟从目的出发一点一点零碎地搜就完全不一样了。

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

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



    我想大多数人都不缺书单,缺的是时间和精力真的去看一遍…… 工作之后自己都有点开始对「为了学习而学习」不屑一顾了
    jorneyr
        56
    jorneyr  
       2022-04-10 08:32:51 +08:00
    每个 IP 最大长度 15 Bytes ,一亿个 IP 的容量为: 100000000 * 15Bytes ,即 1.39G ,现在的硬盘写入速度几个 G/S 的很多,机械硬盘也是 100+M/S ,可以一个线程分段生成,交给另一个异步线程写入 (不要多个线程同时写入)。
    cloverzrg2
        57
    cloverzrg2  
       2022-04-10 11:31:54 +08:00
    多线程写磁盘。。。
    levelworm
        58
    levelworm  
       2022-04-10 11:57:48 +08:00 via Android
    @GeruzoniAnsasu 多谢,果然基础知识重要
    northernlights0
        59
    northernlights0  
       2022-04-10 16:10:47 +08:00
    前面有说不要多线程写入的???除非你用的是机械硬盘才这样,ssd 一直都是并发写效率最高。
    kalluwa
        60
    kalluwa  
       2022-04-10 17:28:28 +08:00
    “你们的真实场景中有将 ip 保存为 32 位整数的吗?”
    这个一般下意识都会选择存 4bytes 的,存字符串的方式按道理说除非有明文要求,都不会去考虑的
    raaaaaar
        61
    raaaaaar  
       2022-04-10 19:56:55 +08:00
    很好,面试官由悄悄 get 一个八股
    xuanbg
        62
    xuanbg  
       2022-04-11 07:02:44 +08:00
    @GeruzoniAnsasu 没错,打好基础后,对新技术稍作了解即可。要用什么新技术了时临时抱佛脚学一下会用就行,用不到的东西没必要花时间去学。所以别人问我要学什么,我都告诉他没有用不要学。。。

    深耕某个领域,自然会步步深入专研,这都不仅仅是学习的范畴了。
    illuz
        63
    illuz  
       2022-04-11 07:48:27 +08:00 via Android
    没人考虑 ipv6 么
    AdminNB
        64
    AdminNB  
       2022-04-11 10:36:30 +08:00
    瞎写了写,不知道对不对



    public class Main {
    @SneakyThrows
    public static void main(String[] args) {
    byte[][] ip = new byte[1000000][4];
    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++) {
    ip[count][0] = (byte) i;
    ip[count][1] = (byte) j;
    ip[count][2] = (byte) k;
    ip[count][3] = (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 字节)
    sbilly
        65
    sbilly  
       2022-04-13 20:04:14 +08:00
    4 * 10^9 byte 没多大吧,1G 也就 26s 的时间。随机性有保障,解析成 IP 读取的时候转换就可以了。。。

    ```
    dd if=/dev/urandom of=/opt/disk2/tmp/ipset.data bs=4096k count=256
    256+0 records in
    256+0 records out
    1073741824 bytes (1.1 GB, 1.0 GiB) copied, 26.4446 s, 40.6 MB/s
    ```
    amigoOS
        66
    amigoOS  
       2022-08-14 18:54:35 +08:00 via Android
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1965 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 00:48 · PVG 08:48 · LAX 16:48 · JFK 19:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.