docker / for-linux

Docker Engine for Linux
https://docs.docker.com/engine/installation/
757 stars 86 forks source link

Messing/Mixing/Duplicating application variables in memory inside running containers. #1337

Closed duktig-dev closed 2 years ago

duktig-dev commented 2 years ago

Expected behavior

Running 4 containers from same image Application running in docker container generates random data and creates json string. Each application running inside containers should generate/create unique values inside docker containers.

Actual behavior

Running 4 containers from same image Applications running inside docker containers mixing a values and almost 70% is the same value. Assuming, issue with containers "shared" memory. Same application(s) running outside of docker container works perfect and generates unique values.

Tested with PHP, Node.js, Golang. The issue is the same.

Steps to reproduce the behavior

Just visit to this repo for more details or run:

git clone https://github.com/duktig-dev/docker-memory-issue-reproduction.git
cd docker-memory-issue-reproduction
docker-compose up -d

In some words you can imagine:

Without docker containers creation, running 4 copies of the same application: Generates unique random values.

Inside docker containers, applications running and 70% of values is the same. Assuming, a problem comes from container's memory stack.

Using docker-compose to deploy containers:

all 4 containers using the same image

container A Using Image "Myimg"
container B Using Image "Myimg"
container C Using Image "Myimg"
container D Using Image "Myimg"

Output of docker version:

Docker version 20.10.12, build e91ed57

Output of docker info:

Client:
 Context:    default
 Debug Mode: false
 Plugins:
  app: Docker App (Docker Inc., v0.9.1-beta3)
  buildx: Docker Buildx (Docker Inc., v0.7.1-docker)
  scan: Docker Scan (Docker Inc., v0.12.0)

Server:
 Containers: 8
  Running: 6
  Paused: 0
  Stopped: 2
 Images: 41
 Server Version: 20.10.12
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc version: v1.0.2-0-g52b36a2
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: default
  cgroupns
 Kernel Version: 5.13.0-22-generic
 Operating System: Ubuntu 21.10
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 15.54GiB
 Name: david-Macmini
 ID: FNY6:BSVD:L7JK:BFCW:ZCMP:6OMK:HM3I:U22T:WB2W:FCXM:4WC5:XS7B
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false

Running the docker in: Ubuntu 21.10 ( bare metal )

akerouanton commented 2 years ago

Hello @duktig-dev, could you try to share a minimal example of a Golang app and a Dockerfile that reproduces this issue please?

duktig-dev commented 2 years ago

Sure!

Dockerfile

FROM golang:1.17

WORKDIR /app
COPY ./src/go.mod ./
COPY ./src/go.sum ./
COPY ./src /app

RUN go mod download

# Exampe template pf build command:
# go build -o {output-bin-file} {source-files-path}
RUN go build -o /events-go-publisher /app/cmd/ 

# RUN go get -d -v ./...
# RUN go install -v ./...

CMD ["/events-go-publisher"]

golang application main file ( other files not included, cos they are not mater)

package main

import (
    "context"
    "fmt"
    "math/rand"
    "os"
    "redis_publisher/config"
    "redis_publisher/lib"
    "sync"
    "time"

    "github.com/go-redis/redis/v8"
)

var ctx = context.Background()
var wg = sync.WaitGroup{}
var logger lib.Logger
var cnf = config.Values
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
var mut sync.Mutex

func main() {
    publish()
}

func publish() {

    rand.Seed(time.Now().Unix())

    fmt.Println("Connecting to Redis Server")
    logger.LogINFO("Connecting to Redis Server")

    var redisClient *redis.Client

    redisClient = redis.NewClient(&redis.Options{
        Addr:     cnf.RedisHostPort,
        Password: cnf.RedisPassword,
        DB:       cnf.RedisDb,
    })

    if _, err := redisClient.Ping(ctx).Result(); err != nil {
        logger.LogError(fmt.Sprintf("[redis] %#v", err))
        panic(fmt.Errorf("[redis] %#v", err))
    }

    i := 1

    f, err := os.OpenFile(fmt.Sprintf("/log/%s.log", cnf.PublisherId), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)

    if err != nil {
        panic(err)
    }

    defer f.Close()

    for {
        select {
        case <-time.After(time.Millisecond * cnf.PublishInterval):

            mut.Lock()

            randomWord := randSeq(10)
            randomNumber := rand.Intn(100) + rand.Intn(100)
            //randomNumber2 := rand.Intn(100)
            randomIdNumber := rand.Intn(100) + rand.Intn(101) + rand.Intn(104)
            randomWordId := randSeq(5)

            payload := fmt.Sprintf(`{"event":"Golang-testing","service":"events.go.publisher.tests","published_time":"%s","data":{"go-publisher":"%s","random-number":%d, "number":%d,"strltr":"test-%s"},"appendix":{"event_id":"EVENT-%s%d"}}`,
                time.Now().Format(time.RFC3339),
                cnf.PublisherId,
                randomNumber,
                i,
                randomWord,
                randomWordId,
                randomIdNumber,
            )

            f.WriteString(payload + "\n")

            if err := redisClient.Publish(ctx, cnf.RedisChannel, payload).Err(); err != nil {
                panic(err)
            }

            mut.Unlock()

            if cnf.PublishAmount > 0 && cnf.PublishAmount == i {
                logger.LogINFO("Finished Publishing from golang")
                os.Exit(0)
            }

            i++

        }
    }

}

func randSeq(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

I'm open for any details.

And saying again, I tested with PHP, Node.js and Golang.

Thank you!

duktig-dev commented 2 years ago

@akerouanton thank you for your attention. as you can see in the golang code, I do publish into Redis and also write content to file for analyzing.

I have written a script to analyze log file content which returning exactly the same values as I do query in PostgreSQL table (where data inserted from Redis subscriber).

In other words, I do the testing with more than one way and results is the same.

With my opinion, there is some issue with Docker containers memory management, with which I'm not familiar with.

Thank you.

duktig-dev commented 2 years ago

@akerouanton to do real reproduction,

Just visit to this repo for more details or run:

git clone https://github.com/duktig-dev/docker-memory-issue-reproduction.git
cd docker-memory-issue-reproduction
docker-compose up -d

Thanks!

duktig-dev commented 2 years ago

The interest thing is that when we trying to generate 10,000 values, the result comes as:

Total: 10000
Unique: 2500
Duplicate: 7500

And we did this try many times.

Also, we tried to add more time interval between data creation. more than 2-3 second.

akerouanton commented 2 years ago

Your code is a bit complex since it involves Redis (it's not a "minimal bug reproducer"). Anyway, I managed to remove these parts and just fmt.Printf random values.

Whether I run this code on my machine or in a container, it produces the same result: if two processes are started at the same time (ie. same unix timestamp), they produce the same output. Actually that's expected: your code relies on rand.Seed to seed the random number generator. If you go look at rand.Seed doc, you can read the following:

Seed uses the provided seed value to initialize the default Source to a deterministic state. [...] Seed values that have the same remainder when divided by 2³¹-1 generate the same pseudo-random sequence.

To circumvent that, you could use time.Now().UnixNano() to seed the PRNG. You could also use crypto/rand.Int() if you need a cryptographically secure RNG and don't mind doing an extra syscall (see man 2 getrandom).

Also note that how rand.Seed()/rand.Intn() behave is not specific to Go. For instance, in PHP, if you seed the PRNG with a predetermined value, you got the exact same values every time you run the script (and that's sometimes useful for testing purposes). PHP also has a cryptographically secure RNG (see random_int).

duktig-dev commented 2 years ago

Hi @akerouanton ! Thank you for response.

For the first, of course I have created a clean/pure code for testing some days ago and commented here: (if you scroll top you will see: Just visit to this repo for more details or run:

git clone https://github.com/duktig-dev/docker-memory-issue-reproduction.git
cd docker-memory-issue-reproduction
docker-compose up -d

)

Anyways,

Yes, because the Docker starts/deploys containers in same time, assuming the random functionality will generate the same values.

I will change the code with your recommendations and report to you.

Thanks again for your attention.

duktig-dev commented 2 years ago

@akerouanton it worked !

Generated 1,000,000 random data and it works perfect!

Starting...
Get file: Golang-Publisher-A.log lines 250000
Get file: Golang-Publisher-B.log lines 250000
Get file: Golang-Publisher-C.log lines 250000
Get file: Golang-Publisher-D.log lines 250000
Total: 1000000
Unique: 1000000
Duplicate: 0

And this a part of improved code:

func getRandomInt64(limit int64) int64 {

    nBig, err := rand.Int(rand.Reader, big.NewInt(limit))
    if err != nil {
        panic(err)
    }
    n := nBig.Int64()
    //fmt.Printf("Here is a random %T in [0,%d) : %d\n", n, limit, n)
    return n
}

func getSmallInt() byte {
    b := []byte{0}
    if _, err := rand.Reader.Read(b); err != nil {
        panic(err)
    }

    return b[0]
}

func getToken(length int) string {
    randomBytes := make([]byte, 32)
    _, err := rand.Read(randomBytes)
    if err != nil {
        panic(err)
    }
    return base32.StdEncoding.EncodeToString(randomBytes)[:length]
}

Thank you very much !!!

thaJeztah commented 2 years ago

Thanks for helping out, @akerouanton !