Skip to content

[Messenger] Added support for auto trimming of redis streams #31825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 2, 2019

Conversation

Toflar
Copy link
Contributor

@Toflar Toflar commented Jun 3, 2019

Q A
Branch? 4.4
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets
License MIT
Doc PR symfony/symfony-docs#11870

Right now the Redis stream will just grow indefinitely. However, there are means to have it delete old entries from time to time.
Note: I could not use the XADD mystream MAXLEN ~ 1000 * notation because the PHP redis extension does not support the MAXLEN option afaics so I went for the extra XTRIM command.
I explicitly enabled the approximate flag because it makes absolutely no sense to hardcode the limit for us although we could even have this configurable too (but I don't think we should).
The whole idea of this PR is to enable occasional trimming of the stream so it doesn't grow forever, so when you configure something like 20000 it may well happen that trimming only happens at 25000 depending on your settings.

Ping @soyuka @alexander-schranz @chalasr :)

@Tobion
Copy link
Contributor

Tobion commented Jun 3, 2019

So this is similar to TTL in rabbitmq: https://www.rabbitmq.com/ttl.html
Can't we use redis TTL for this as well? Having random trims of random old items seems strange to me.

If we can implement it for redis and doctrine, we could add a generic ExpirationStamp that could work for all transports.

@nicolas-grekas nicolas-grekas added this to the next milestone Jun 3, 2019
@Toflar
Copy link
Contributor Author

Toflar commented Jun 3, 2019

There is no such thing as TTL in Redis Streams :)

@jderusse
Copy link
Member

jderusse commented Jun 3, 2019

Redis stream's ID are timestamp, you may run a script to remove older items

local t = redis.call('TIME') 
local keys=redis.call('XRANGE', ARGV[1], '-', (t[1]-ARGV[2]) * 1000) 
for i=1,#keys,1 do 
  redis.call('XDEL', ARGV[1], keys[i][1]) 
end 
return #keys

with ARGV[1] the name of the stream and ARGV[2] the TTL in seconds

note: I'm not an expert of redis, I don't know the performance impact of such script.

@Toflar
Copy link
Contributor Author

Toflar commented Jun 3, 2019

There‘s a reason why TTL support is not built-in in Redis (yet). You can read more about it here: https://github.com/antirez/redis/issues/4450
The maxlength feature is the only way to trim it as of today so I think this one has to be Redis-specific.

@Tobion
Copy link
Contributor

Tobion commented Jun 3, 2019

I don't see the need to implement that as a core feature. If messages need to have a ttl then people should use the tools that support it, e.g. rabbitmq. If it's purely about local development where you might not run the workers, then it would be enough to have a script that deletes old items from redis that you can call manually or in a cron or whatever people prefer.
In it's current form it just deletes arbitrary data. Do you want to enable that in prod? What's the use-case?

@jderusse
Copy link
Member

jderusse commented Jun 4, 2019

I don't see the need to implement that as a core feature.

@Tobion with redis implémentation, consumed message are not deleted. This PR is not about providing a feature to delete message. But about cleaning the server.

2 options:

  • using a MAXLEN (fast, but not reliable, because redis may delete unconsumed messages when you push too much messages in the queue)
  • using a TTL (slow, may block the redis thread during the garbage collection, and can consume a lot of memory with high throughput producer)

@Toflar I'm aware of that issue. When using redis stream as a messages storage there is a trade off between performance and reliability

In my experience I already had situation where consumers were out during few hours and queue grown 100 000 times bigger than usual. In such situation, capping with MAXLEN would have lost messages or would requires to set a extremely high MAXLEN (which will consume a lot of space even when the situation is ok)

Maybe we can consider combining the 2 solutions: counting outdated elements with XRANGE and using XLEN to define a new MAXLEN. ie.

local t = redis.call('TIME')
local keys=redis.call('XRANGE', ARGV[1], '-', (t[1]-ARGV[2]) * 1000, 'COUNT', ARGV[3])
if #keys > 0 then
  local l = redis.call('XLEN', ARGV[1])
  redis.call('XTRIM', ARGV[1], 'MAXLEN', '~', l-#keys)
  return #keys
else
  return 0
end"

what do you think?

@Toflar
Copy link
Contributor Author

Toflar commented Jun 4, 2019

Do you want to enable that in prod? What's the use-case?

The use case is that I already have Redis running and Streams are pretty much explicitly built for message handling. Why would I want to manage yet another tool if Redis can serve me well? 😄

In my experience I already had situation where consumers were out during few hours and queue grown 100 000 times bigger than usual. In such situation, capping with MAXLEN would have lost messages or would requires to set a extremely high MAXLEN (which will consume a lot of space even when the situation is ok)

Hmmm, so I see why my PR does not seem practical under certain circumstances.
So maybe we have to go a different path here and provide an explicit cleanup command?

These are the use cases I see:

  • Purge all messages, no matter the age and status
  • Purge messages that are older than <dateinterval> no matter the status
  • Purge messages that are older than <dateinterval> but only handled ones, not pending
  • Purge messages of all age but only handled ones.

And all of those optionally restricted to receivers.

So I don't know but maybe something like

  • bin/console messenger:cleanup <receivers>
  • bin/console messenger:cleanup --older-than=P7D <receivers>
  • bin/console messenger:cleanup --older-than=P7D --handled <receivers>
  • bin/console messenger:cleanup --handled <receivers>

(I did not really think about naming and defaults, I just think aloud here.)
And then we'll have an interface and every transport can implement that and optimize for the best way in their case. Also, this would ensure things happen in a separate cleanup process that (in best case) do not affect production performance.

Does this seem like a better approach?

@jderusse
Copy link
Member

jderusse commented Jun 4, 2019

hmm. I'm not sure about a dedicated command, because: running a periodic small cleanup (like you did with the trimProbability) or a big one every X minutes, will, in both case, block the Redis thread and block every consumers. BUT, the more you call the trim algorithm, the smaller is the subset of messages to remove, and the shorter will be the blocking time window.

FYI, I've made a small bench with very simple data (I don't know if the result will be the same with larger messages) it takes ~50ms to remove a small subset of ~10 000 (that's why I was thinking about removing the oldest 2 000 messages every 1 000 XADD)

But thinking about that, I really wonder if such complexity worth it. Maybe, I'm just exposing an edge case, and your first idea is the good one. We may warned user in the documentation, and recommend to use a real messaging bus for high throughput cases.

@alexander-schranz
Copy link
Contributor

alexander-schranz commented Jun 4, 2019

@Toflar

Note: I could not use the XADD mystream MAXLEN ~ 1000 * notation because the PHP redis extension does not support the MAXLEN option afaics so I went for the extra XTRIM command.

The 4 parameter of the xadd function is the maxlen e.g.:

$redis->xadd("new3", "*", ["key" => "value"], 10);

its just missing in the phpdocs (my fault 🙈 ).

@Toflar
Copy link
Contributor Author

Toflar commented Jun 4, 2019

Ah 😄 Well, that can be easily changed. Anything you want to add to the discussion?

@alexander-schranz
Copy link
Contributor

alexander-schranz commented Jun 11, 2019

@Toflar I think by default we should not autotrim because a default value is difficult to set here, but it would be good to have a way to configure this. Is the performance better when using xadd? The 5th argument would be also a boolean flag which represents the approximate (~) operator if set to true so the trim_probability could be removed and used the redis internal mode for this.

@Toflar
Copy link
Contributor Author

Toflar commented Jun 11, 2019

I think by default we should not autotrim because a default value is difficult to set here

Agreed.

Is the performance better when using xadd?

I have absolutely no clue. I don't know which is better, xadd with MAXLEN ~ all the time or an xtrim every now and then.

@alexander-schranz
Copy link
Contributor

A simple bench the xadd seems to be a better choice:

Testvalues adding 100000 messages with a trim size of 100:

normal xAdd: 4.6s
xAdd with approximate flag: 4.7s
xAdd with fixed size: 5.2s
xAdd with xtrim: 8.9s

So I think we should go with:

$this->connection->xadd($this->stream, '*', ..., $this->maxlength, (bool) $this->maxlength);

As its optional I think its ok to implement this for the redis transport?

/cc @sroze @chalasr

@Toflar
Copy link
Contributor Author

Toflar commented Jun 12, 2019

Thanks for benchmarking 👍 I'll update the PR accordingly but I'd like to wait for feedback by Sam and Robin.

@chalasr
Copy link
Member

chalasr commented Jun 17, 2019

Thanks for raising the issue @Toflar.
👍 for passing an approximate max length to xadd.

Copy link
Contributor

@sroze sroze left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 👍

@sroze sroze force-pushed the redis-messenger-transport-max-length branch from 1942fec to 7fe06bc Compare July 2, 2019 20:18
@sroze
Copy link
Contributor

sroze commented Jul 2, 2019

Thank you @Toflar.

@sroze sroze merged commit 7fe06bc into symfony:4.4 Jul 2, 2019
sroze added a commit that referenced this pull request Jul 2, 2019
…treams (Toflar)

This PR was squashed before being merged into the 4.4 branch (closes #31825).

Discussion
----------

[Messenger] Added support for auto trimming of redis streams

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets |
| License       | MIT
| Doc PR        | will submit if concept is okay

Right now the Redis stream will just grow indefinitely. However, there are means to have it delete old entries from time to time.
Note: I could not use the `XADD mystream MAXLEN ~ 1000 *` notation because the PHP redis extension does not support the `MAXLEN` option afaics so I went for the extra `XTRIM` command.
I explicitly enabled the approximate flag because it makes absolutely no sense to hardcode the limit for us although we could even have this configurable too (but I don't think we should).
The whole idea of this PR is to enable occasional trimming of the stream so it doesn't grow forever, so when you configure something like `20000` it may well happen that trimming only happens at `25000` depending on your settings.

Ping @soyuka @alexander-schranz @chalasr :)

Commits
-------

7fe06bc [Messenger] Added support for auto trimming of redis streams
@Toflar Toflar deleted the redis-messenger-transport-max-length branch July 3, 2019 06:21
javiereguiluz added a commit to javiereguiluz/symfony-docs that referenced this pull request Jul 3, 2019
… option (Toflar)

This PR was submitted for the master branch but it was merged into the 4.4 branch instead (closes symfony#11870).

Discussion
----------

Documented redis transport new stream_max_entries option

Docs for symfony/symfony#31825

Commits
-------

d1aa071 Documented redis transport new stream_max_entries option
@nicolas-grekas nicolas-grekas modified the milestones: next, 4.4 Oct 27, 2019
This was referenced Nov 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants